Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/userguide/miscellaneous.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ These include all :term:`pure Python modules <Pure Module>` in the
headers) listed as part of extensions when creating a :term:`source
distribution (or "sdist")`.

.. note::
.. versionadded:: v68.3.0
``setuptools`` will attempt to include type information files
by default in the distribution
(``.pyi`` and ``py.typed``, as specified in :pep:`561`).

*Please note however that this feature is* **EXPERIMENTAL** *and may change in
the future.*

If you have ``.pyi`` and ``py.typed`` files in your project, but do not
wish to distribute them, you can opt out by setting
:doc:`exclude-package-data </userguide/datafiles>` to remove them.

However, when building more complex packages (e.g. packages that include
non-Python files, or that need to use custom C headers), you might find that
not all files present in your project folder are included in package
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/3136.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Include type information (``py.typed``, ``*.pyi``) by default (#3136) -- by :user:`Danie-1`,
**EXPERIMENTAL**.
7 changes: 6 additions & 1 deletion setuptools/command/build_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
from ..warnings import SetuptoolsDeprecationWarning


_IMPLICIT_DATA_FILES = ('*.pyi', 'py.typed')


def make_writable(target):
os.chmod(target, os.stat(target).st_mode | stat.S_IWRITE)

Expand Down Expand Up @@ -116,6 +119,7 @@ def find_data_files(self, package, src_dir):
self.package_data,
package,
src_dir,
extra_patterns=_IMPLICIT_DATA_FILES,
)
globs_expanded = map(partial(glob, recursive=True), patterns)
# flatten the expanded globs into an iterable of matches
Expand Down Expand Up @@ -285,14 +289,15 @@ def exclude_data_files(self, package, src_dir, files):
return list(unique_everseen(keepers))

@staticmethod
def _get_platform_patterns(spec, package, src_dir):
def _get_platform_patterns(spec, package, src_dir, extra_patterns=[]):
"""
yield platform-specific path patterns (suitable for glob
or fn_match) from a glob-based spec (such as
self.package_data or self.exclude_package_data)
matching package in src_dir.
"""
raw_patterns = itertools.chain(
extra_patterns,
spec.get('', []),
spec.get(package, []),
)
Expand Down
6 changes: 6 additions & 0 deletions setuptools/tests/test_build_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,10 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script):
"src": {
"foo": {
"__init__.py": "__version__ = '0.1'",
"__init__.pyi": "__version__: str",
"cli.py": "def main(): print('hello world')",
"data.txt": "def main(): print('hello world')",
"py.typed": "",
}
},
}
Expand Down Expand Up @@ -406,8 +408,10 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script):
'foo-0.1/src',
'foo-0.1/src/foo',
'foo-0.1/src/foo/__init__.py',
'foo-0.1/src/foo/__init__.pyi',
'foo-0.1/src/foo/cli.py',
'foo-0.1/src/foo/data.txt',
'foo-0.1/src/foo/py.typed',
'foo-0.1/src/foo.egg-info',
'foo-0.1/src/foo.egg-info/PKG-INFO',
'foo-0.1/src/foo.egg-info/SOURCES.txt',
Expand All @@ -419,8 +423,10 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script):
}
assert wheel_contents == {
"foo/__init__.py",
"foo/__init__.pyi", # include type information by default
"foo/cli.py",
"foo/data.txt", # include_package_data defaults to True
"foo/py.typed", # include type information by default
"foo-0.1.dist-info/LICENSE.txt",
"foo-0.1.dist-info/METADATA",
"foo-0.1.dist-info/WHEEL",
Expand Down
139 changes: 139 additions & 0 deletions setuptools/tests/test_build_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,142 @@ def test_get_outputs(tmpdir_cwd):
f"{build_lib}/mypkg/sub2/nested/__init__.py": "other/__init__.py",
f"{build_lib}/mypkg/sub2/nested/mod3.py": "other/mod3.py",
}


class TestTypeInfoFiles:
PYPROJECTS = {
"default_pyproject": DALS(
"""
[project]
name = "foo"
version = "1"
"""
),
"dont_include_package_data": DALS(
"""
[project]
name = "foo"
version = "1"

[tools.setuptools]
include-package-data = false
"""
),
"exclude_type_info": DALS(
"""
[project]
name = "foo"
version = "1"

[tools.setuptools]
include-package-data = false

[tool.setuptools.exclude-package-data]
"*" = ["py.typed", "*.pyi"]
"""
),
}

EXAMPLES = {
"simple_namespace": {
"directory_structure": {
"foo": {
"bar.pyi": "",
"py.typed": "",
"__init__.py": "",
}
},
"expected_type_files": {"foo/bar.pyi", "foo/py.typed"},
},
"nested_inside_namespace": {
"directory_structure": {
"foo": {
"bar": {
"py.typed": "",
"mod.pyi": "",
}
}
},
"expected_type_files": {"foo/bar/mod.pyi", "foo/bar/py.typed"},
},
"namespace_nested_inside_regular": {
"directory_structure": {
"foo": {
"namespace": {
"foo.pyi": "",
},
"__init__.pyi": "",
"py.typed": "",
}
},
"expected_type_files": {
"foo/namespace/foo.pyi",
"foo/__init__.pyi",
"foo/py.typed",
},
},
}

@pytest.mark.parametrize(
"pyproject", ["default_pyproject", "dont_include_package_data"]
)
@pytest.mark.parametrize("example", EXAMPLES.keys())
def test_type_files_included_by_default(self, tmpdir_cwd, pyproject, example):
structure = {
**self.EXAMPLES[example]["directory_structure"],
"pyproject.toml": self.PYPROJECTS[pyproject],
}
expected_type_files = self.EXAMPLES[example]["expected_type_files"]
jaraco.path.build(structure)

build_py = get_finalized_build_py()
outputs = get_outputs(build_py)
assert expected_type_files <= outputs

@pytest.mark.parametrize("pyproject", ["exclude_type_info"])
@pytest.mark.parametrize("example", EXAMPLES.keys())
def test_type_files_can_be_excluded(self, tmpdir_cwd, pyproject, example):
structure = {
**self.EXAMPLES[example]["directory_structure"],
"pyproject.toml": self.PYPROJECTS[pyproject],
}
expected_type_files = self.EXAMPLES[example]["expected_type_files"]
jaraco.path.build(structure)

build_py = get_finalized_build_py()
outputs = get_outputs(build_py)
assert expected_type_files.isdisjoint(outputs)

def test_stub_only_package(self, tmpdir_cwd):
structure = {
"pyproject.toml": DALS(
"""
[project]
name = "foo-stubs"
version = "1"
"""
),
"foo-stubs": {"__init__.pyi": "", "bar.pyi": ""},
}
expected_type_files = {"foo-stubs/__init__.pyi", "foo-stubs/bar.pyi"}
jaraco.path.build(structure)

build_py = get_finalized_build_py()
outputs = get_outputs(build_py)
assert expected_type_files <= outputs


def get_finalized_build_py(script_name="%build_py-test%"):
dist = Distribution({"script_name": script_name})
dist.parse_config_files()
build_py = dist.get_command_obj("build_py")
build_py.finalize_options()
return build_py


def get_outputs(build_py):
build_dir = Path(build_py.build_lib)
return {
os.path.relpath(x, build_dir).replace(os.sep, "/")
for x in build_py.get_outputs()
}