diff --git a/poetry/core/masonry/utils/package_include.py b/poetry/core/masonry/utils/package_include.py index 512c6c48f..47345765f 100644 --- a/poetry/core/masonry/utils/package_include.py +++ b/poetry/core/masonry/utils/package_include.py @@ -33,6 +33,21 @@ def refresh(self): # type: () -> PackageInclude return self.check_elements() + def is_stub_only(self): # type: () -> bool + # returns `True` if this a PEP 561 stub-only package, + # see [PEP 561](https://www.python.org/dev/peps/pep-0561/#stub-only-packages) + return self.package.endswith("-stubs") and all( + el.suffix == ".pyi" + or (el.parent.name == self.package and el.name == "py.typed") + for el in self.elements + if el.is_file() + ) + + def has_modules(self): # type: () -> bool + # Packages no longer need an __init__.py in python3, but there must + # at least be one .py file for it to be considered a package + return any(element.suffix == ".py" for element in self.elements) + def check_elements(self): # type: () -> PackageInclude if not self._elements: raise ValueError( @@ -43,20 +58,18 @@ def check_elements(self): # type: () -> PackageInclude if len(self._elements) > 1: # Probably glob self._is_package = True + self._package = root.parent.name - # Packages no longer need an __init__.py in python3, but there must - # at least be one .py file for it to be considered a package - if not any([element.suffix == ".py" for element in self._elements]): + if not self.is_stub_only() and not self.has_modules(): raise ValueError("{} is not a package.".format(root.name)) - self._package = root.parent.name else: if root.is_dir(): # If it's a directory, we include everything inside it self._package = root.name self._elements = sorted(list(root.glob("**/*"))) - if not any([element.suffix == ".py" for element in self._elements]): + if not self.is_stub_only() and not self.has_modules(): raise ValueError("{} is not a package.".format(root.name)) self._is_package = True diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/__init__.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/module.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/module.pyi new file mode 100644 index 000000000..d79e6e39e --- /dev/null +++ b/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/module.pyi @@ -0,0 +1,4 @@ +"""Example module""" +from typing import Tuple + +version_info = Tuple[int, int, int] diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/subpkg/__init__.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only/pkg-stubs/subpkg/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only/pyproject.toml b/tests/masonry/builders/fixtures/pep_561_stub_only/pyproject.toml new file mode 100644 index 000000000..36a077afd --- /dev/null +++ b/tests/masonry/builders/fixtures/pep_561_stub_only/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "pep-561-stubs" +version = "0.1" +description = "PEP 561 stub package example" +authors = [ + "Oleg Höfling " +] +license = "MIT" +packages = [ + {include = "pkg-stubs"} +] + +[tool.poetry.dependencies] +python = "^3.6" diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/__init__.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/module.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/module.pyi new file mode 100644 index 000000000..d79e6e39e --- /dev/null +++ b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/module.pyi @@ -0,0 +1,4 @@ +"""Example module""" +from typing import Tuple + +version_info = Tuple[int, int, int] diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/py.typed b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/py.typed new file mode 100644 index 000000000..b648ac923 --- /dev/null +++ b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/py.typed @@ -0,0 +1 @@ +partial diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/subpkg/__init__.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pkg-stubs/subpkg/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pyproject.toml b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pyproject.toml new file mode 100644 index 000000000..db202c04a --- /dev/null +++ b/tests/masonry/builders/fixtures/pep_561_stub_only_partial/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "pep-561-stubs" +version = "0.1" +description = "PEP 561 stub package example with the py.typed marker file" +authors = [ + "Oleg Höfling " +] +license = "MIT" +packages = [ + {include = "pkg-stubs"} +] + +[tool.poetry.dependencies] +python = "^3.6" diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_src/pyproject.toml b/tests/masonry/builders/fixtures/pep_561_stub_only_src/pyproject.toml new file mode 100644 index 000000000..666b2b24f --- /dev/null +++ b/tests/masonry/builders/fixtures/pep_561_stub_only_src/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "pep-561-stubs" +version = "0.1" +description = "PEP 561 stub package example with an src layout" +authors = [ + "Oleg Höfling " +] +license = "MIT" +packages = [ + {include = "pkg-stubs", from = "src"} +] + +[tool.poetry.dependencies] +python = "^3.6" diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/__init__.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/module.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/module.pyi new file mode 100644 index 000000000..d79e6e39e --- /dev/null +++ b/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/module.pyi @@ -0,0 +1,4 @@ +"""Example module""" +from typing import Tuple + +version_info = Tuple[int, int, int] diff --git a/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/subpkg/__init__.pyi b/tests/masonry/builders/fixtures/pep_561_stub_only_src/src/pkg-stubs/subpkg/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/builders/test_sdist.py b/tests/masonry/builders/test_sdist.py index 8a182ac54..cdec918b0 100644 --- a/tests/masonry/builders/test_sdist.py +++ b/tests/masonry/builders/test_sdist.py @@ -506,3 +506,21 @@ def test_excluded_subpackage(): exec(compile(setup_ast, filename="setup.py", mode="exec"), ns) assert ns["packages"] == ["example"] + + +def test_sdist_package_pep_561_stub_only(): + root = fixtures_dir / "pep_561_stub_only" + poetry = Factory().create_poetry(root) + + builder = SdistBuilder(poetry) + builder.build() + + sdist = root / "dist" / "pep-561-stubs-0.1.tar.gz" + + assert sdist.exists() + + with tarfile.open(str(sdist), "r") as tar: + names = tar.getnames() + assert "pep-561-stubs-0.1/pkg-stubs/__init__.pyi" in names + assert "pep-561-stubs-0.1/pkg-stubs/module.pyi" in names + assert "pep-561-stubs-0.1/pkg-stubs/subpkg/__init__.pyi" in names diff --git a/tests/masonry/builders/test_wheel.py b/tests/masonry/builders/test_wheel.py index b8bbd2080..294047a6c 100644 --- a/tests/masonry/builders/test_wheel.py +++ b/tests/masonry/builders/test_wheel.py @@ -155,3 +155,33 @@ def test_dist_info_file_permissions(): z.getinfo("my_package-1.2.3.dist-info/entry_points.txt").external_attr == 0o644 << 16 ) + + +@pytest.mark.parametrize( + "package", + ["pep_561_stub_only", "pep_561_stub_only_partial", "pep_561_stub_only_src"], +) +def test_wheel_package_pep_561_stub_only(package): + root = fixtures_dir / package + WheelBuilder.make(Factory().create_poetry(root)) + + whl = root / "dist" / "pep_561_stubs-0.1-py3-none-any.whl" + + assert whl.exists() + + with zipfile.ZipFile(str(whl)) as z: + assert "pkg-stubs/__init__.pyi" in z.namelist() + assert "pkg-stubs/module.pyi" in z.namelist() + assert "pkg-stubs/subpkg/__init__.pyi" in z.namelist() + + +def test_wheel_package_pep_561_stub_only_includes_typed_marker(): + root = fixtures_dir / "pep_561_stub_only_partial" + WheelBuilder.make(Factory().create_poetry(root)) + + whl = root / "dist" / "pep_561_stubs-0.1-py3-none-any.whl" + + assert whl.exists() + + with zipfile.ZipFile(str(whl)) as z: + assert "pkg-stubs/py.typed" in z.namelist() diff --git a/tests/masonry/utils/fixtures/pep_561_stub_only/bad/__init__.pyi b/tests/masonry/utils/fixtures/pep_561_stub_only/bad/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/utils/fixtures/pep_561_stub_only/bad/module.pyi b/tests/masonry/utils/fixtures/pep_561_stub_only/bad/module.pyi new file mode 100644 index 000000000..f85a07d46 --- /dev/null +++ b/tests/masonry/utils/fixtures/pep_561_stub_only/bad/module.pyi @@ -0,0 +1,5 @@ +"""Example module""" +from typing import Tuple + + +version_info = Tuple[int, int, int] diff --git a/tests/masonry/utils/fixtures/pep_561_stub_only/good-stubs/__init__.pyi b/tests/masonry/utils/fixtures/pep_561_stub_only/good-stubs/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/tests/masonry/utils/fixtures/pep_561_stub_only/good-stubs/module.pyi b/tests/masonry/utils/fixtures/pep_561_stub_only/good-stubs/module.pyi new file mode 100644 index 000000000..f85a07d46 --- /dev/null +++ b/tests/masonry/utils/fixtures/pep_561_stub_only/good-stubs/module.pyi @@ -0,0 +1,5 @@ +"""Example module""" +from typing import Tuple + + +version_info = Tuple[int, int, int] diff --git a/tests/masonry/utils/test_package_include.py b/tests/masonry/utils/test_package_include.py index 18acb914b..0db1ff6da 100644 --- a/tests/masonry/utils/test_package_include.py +++ b/tests/masonry/utils/test_package_include.py @@ -50,3 +50,20 @@ def test_package_include_with_non_existent_directory(): err_str = str(with_includes / "not_a_dir") + " does not contain any element" assert str(e.value) == err_str + + +def test_pep_561_stub_only_package_good_name_suffix(): + pkg_include = PackageInclude( + base=fixtures_dir / "pep_561_stub_only", include="good-stubs" + ) + assert pkg_include.elements == [ + fixtures_dir / "pep_561_stub_only/good-stubs/__init__.pyi", + fixtures_dir / "pep_561_stub_only/good-stubs/module.pyi", + ] + + +def test_pep_561_stub_only_package_bad_name_suffix(): + with pytest.raises(ValueError) as e: + PackageInclude(base=fixtures_dir / "pep_561_stub_only", include="bad") + + assert str(e.value) == "bad is not a package."