Repository: takluyver/flit
Pull request #260 (2019-04-24 03:50:24)

WIP: allow src dir
I'd really like to use flit for projects with src directories.

The flit project already has 2 types of projects.

1. just a module, like `foo.py`
2. a package (directory with `__init__.py`), like `foo/__init__.py`

This would add a 3rd and 4th. 

3. just a module, but in src, like `src/foo.py`
4. a package in src, like `src/foo/__init__.py`


It has minimal impact on the rest of the code, only touching 2 functions.
All existing tests still pass.


Patch:
diff --git a/.gitignore b/.gitignore
index 9ff39fc..49234a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ __pycache__/
 /.coverage
 /.pytest_cache
 /.tox
+.idea/
+venv/
diff --git a/flit/common.py b/flit/common.py
index 178afb7..fb0c78a 100644
--- a/flit/common.py
+++ b/flit/common.py
@@ -19,14 +19,28 @@ class Module(object):
         # It must exist either as a .py file or a directory, but not both
         pkg_dir = Path(directory, name)
         py_file = Path(directory, name+'.py')
+        src_dir = Path(directory) / 'src'
+        src_pkg_dir = Path(src_dir, name)
+        src_py_file = Path(src_dir, name+'.py')
         if pkg_dir.is_dir() and py_file.is_file():
             raise ValueError("Both {} and {} exist".format(pkg_dir, py_file))
+        if src_pkg_dir.is_dir() and src_py_file.is_file():
+            raise ValueError("Both {} and {} exist".format(src_pkg_dir, src_py_file))
+        if ((py_file.is_file() or pkg_dir.is_dir()) and
+            (src_py_file.is_file() or src_pkg_dir.is_dir())):
+            raise ValueError("Both src and non-src versions of {} exist".format(name))
         elif pkg_dir.is_dir():
             self.path = pkg_dir
             self.is_package = True
         elif py_file.is_file():
             self.path = py_file
             self.is_package = False
+        elif src_pkg_dir.is_dir():
+            self.path = src_pkg_dir
+            self.is_package = True
+        elif src_py_file.is_file():
+            self.path = src_py_file
+            self.is_package = False
         else:
             raise ValueError("No file/folder found for module {}".format(name))
 
diff --git a/flit/init.py b/flit/init.py
index 2fc52ac..49f99e6 100644
--- a/flit/init.py
+++ b/flit/init.py
@@ -84,6 +84,20 @@ class IniterBase:
                 if p.stem not in {'setup'} and not p.name.startswith('test_'):
                     modules.append(p.stem)
 
+        src_dir = self.directory / 'src'
+        if src_dir.is_dir():
+            for p in src_dir.iterdir():
+                if not p.stem.isidentifier():
+                    continue
+
+                if p.is_dir() and (p / '__init__.py').is_file():
+                    if p.name not in {'test', 'tests'}:
+                        packages.append(p.name)
+
+                elif p.is_file() and p.suffix == '.py':
+                    if p.stem not in {'setup'} and not p.name.startswith('test_'):
+                        modules.append(p.stem)
+
         if len(packages) == 1:
             return packages[0]
         elif len(packages) == 0 and len(modules) == 1:


Test patch:
diff --git a/tests/samples/module3-pkg.ini b/tests/samples/module3-pkg.ini
new file mode 100644
index 0000000..a2bd604
--- /dev/null
+++ b/tests/samples/module3-pkg.ini
@@ -0,0 +1,5 @@
+[metadata]
+module=module3
+author=Sir Robin
+author-email=robin@camelot.uk
+home-page=http://github.com/sirrobin/module3
diff --git a/tests/samples/package2/package2-pkg.ini b/tests/samples/package2/package2-pkg.ini
new file mode 100644
index 0000000..3b0864d
--- /dev/null
+++ b/tests/samples/package2/package2-pkg.ini
@@ -0,0 +1,8 @@
+[metadata]
+module=package2
+author=Sir Robin
+author-email=robin@camelot.uk
+home-page=http://github.com/sirrobin/package2
+
+[scripts]
+pkg_script=package2:main
diff --git a/tests/samples/package2/src/package2/__init__.py b/tests/samples/package2/src/package2/__init__.py
new file mode 100644
index 0000000..07978d8
--- /dev/null
+++ b/tests/samples/package2/src/package2/__init__.py
@@ -0,0 +1,6 @@
+"""A sample package"""
+
+__version__ = '0.1'
+
+def main():
+    print("package1 main")
diff --git a/tests/samples/package2/src/package2/foo.py b/tests/samples/package2/src/package2/foo.py
new file mode 100644
index 0000000..1337a53
--- /dev/null
+++ b/tests/samples/package2/src/package2/foo.py
@@ -0,0 +1,1 @@
+a = 1
diff --git a/tests/samples/src/module3.py b/tests/samples/src/module3.py
new file mode 100644
index 0000000..87f0370
--- /dev/null
+++ b/tests/samples/src/module3.py
@@ -0,0 +1,3 @@
+"""Example module"""
+
+__version__ = '0.1'
diff --git a/tests/test_init.py b/tests/test_init.py
index 6d6fbe9..741be4d 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -67,10 +67,23 @@ def test_guess_module_name():
         ib = init.IniterBase(td)
         assert ib.guess_module_name() == 'baz'
 
+    with make_dir(['src/foo.py', 'src/foo-bar.py', 'test_foo.py', 'setup.py'],
+                  ['src',]) as td:
+        ib = init.IniterBase(td)
+        assert ib.guess_module_name() == 'foo'
+
+    with make_dir(['src/baz/__init__.py', 'tests/__init__.py'], ['src', 'src/baz', 'tests']) as td:
+        ib = init.IniterBase(td)
+        assert ib.guess_module_name() == 'baz'
+
     with make_dir(['foo.py', 'bar.py']) as td:
         ib = init.IniterBase(td)
         assert ib.guess_module_name() is None
 
+    with make_dir(['src/foo.py', 'src/bar.py'], ['src']) as td:
+        ib = init.IniterBase(td)
+        assert ib.guess_module_name() is None
+
 def test_write_license():
     with TemporaryDirectory() as td:
         ib = init.IniterBase(td)
diff --git a/tests/test_wheel.py b/tests/test_wheel.py
index e5cc1b1..1a3c537 100644
--- a/tests/test_wheel.py
+++ b/tests/test_wheel.py
@@ -20,6 +20,7 @@ def clear_samples_dist():
     except FileNotFoundError:
         pass
 
+
 def unpack(path):
     z = zipfile.ZipFile(str(path))
     t = tempfile.TemporaryDirectory()
@@ -36,6 +37,22 @@ def test_wheel_package():
     wheel_main(samples_dir / 'package1-pkg.ini')
     assert_isfile(samples_dir / 'dist/package1-0.1-py2.py3-none-any.whl')
 
+def test_wheel_src_module():
+    clear_samples_dist()
+    wheel_main(samples_dir / 'module3-pkg.ini')
+    assert_isfile(samples_dir / 'dist/module3-0.1-py2.py3-none-any.whl')
+
+def clear_package2_dist():
+    try:
+        shutil.rmtree(str(samples_dir / 'package2' /'dist'))
+    except FileNotFoundError:
+        pass
+
+def test_wheel_src_package():
+    wheel_main(samples_dir / 'package2' / 'package2-pkg.ini')
+    assert_isfile(samples_dir / 'package2' / 'dist/package2-0.1-py2.py3-none-any.whl')
+    clear_package2_dist()
+
 def test_dist_name():
     clear_samples_dist()
     wheel_main(samples_dir / 'altdistname.ini')