Skip to content

Commit

Permalink
Include type information by default (#4021)
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri committed Oct 16, 2023
2 parents 9ece3c9 + 42fc47e commit 2384d91
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
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()
}

0 comments on commit 2384d91

Please sign in to comment.