diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7168992 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,71 @@ +name: test + +on: + push: + branches: + - master + - "test-me-*" + tags: + - "*" + pull_request: + branches: + - master + +# Set permissions at the job level. +permissions: {} + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Lint + run: tox -e lint + + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + strategy: + fail-fast: false + matrix: + python: [ + "3.8", + "3.9", + "3.10", + "3.11", + "3.12", + "3.13", + ] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Test + run: tox -e py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8303af2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f1f4b7b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -language: python -# cache package wheels (1 cache per python version) -cache: pip -# newer python versions are available only on xenial (while some older only on trusty) Ubuntu distribution -dist: trusty - -env: - TOXENV=py - -jobs: - include: - - python: 3.5.0 - - python: 3.5.1 - - python: 3.5.2 - - python: 3.5.3 - - python: 3.5 - - python: 3.6.0 - - python: 3.6.1 - - python: 3.6.2 - - python: 3.6 - dist: xenial - - python: 3.7.0 - dist: xenial - - python: 3.7.1 - dist: xenial - - python: 3.7 - dist: xenial - - python: 3.8 - dist: xenial - - python: 3.9 - dist: xenial - - - name: "check code style with flake8" - python: 3.7 - dist: xenial - env: - - TOXENV=lint - -install: -- pip install -U pip setuptools -- pip install -U tox -- tox --notest - -script: -- tox diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 1aba38f..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE diff --git a/mypy_extensions.py b/mypy_extensions.py index 6600b21..1910000 100644 --- a/mypy_extensions.py +++ b/mypy_extensions.py @@ -5,7 +5,7 @@ from mypy_extensions import TypedDict """ -from typing import Any +from typing import Any, Dict import sys # _type_check is NOT a part of public typing API, it is used here only to mimic @@ -42,17 +42,32 @@ def _typeddict_new(cls, _typename, _fields=None, **kwargs): except (AttributeError, ValueError): pass - return _TypedDictMeta(_typename, (), ns) + return _TypedDictMeta(_typename, (), ns, _from_functional_call=True) class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, total=True): + def __new__(cls, name, bases, ns, total=True, _from_functional_call=False): # Create new typed dict class object. # This method is called directly when TypedDict is subclassed, # or via _typeddict_new when TypedDict is instantiated. This way # TypedDict supports all three syntaxes described in its docstring. # Subclasses and instances of TypedDict return actual dictionaries # via _dict_new. + + # We need the `if TypedDict in globals()` check, + # or we emit a DeprecationWarning when creating mypy_extensions.TypedDict itself + if 'TypedDict' in globals(): + import warnings + warnings.warn( + ( + "mypy_extensions.TypedDict is deprecated, " + "and will be removed in a future version. " + "Use typing.TypedDict or typing_extensions.TypedDict instead." + ), + DeprecationWarning, + stacklevel=(3 if _from_functional_call else 2) + ) + ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns) @@ -135,7 +150,8 @@ def KwArg(type=Any): # Return type that indicates a function does not return -class NoReturn: pass +# Deprecated, use typing or typing_extensions variants instead +class _DEPRECATED_NoReturn: pass def trait(cls): @@ -211,3 +227,25 @@ def __new__(cls, x=0, base=_sentinel): * isinstance(x, {name}) is the same as isinstance(x, int) """.format(name=_int_type.__name__) del _int_type + + +def _warn_deprecation(name: str, module_globals: Dict[str, Any]) -> Any: + if (val := module_globals.get(f"_DEPRECATED_{name}")) is None: + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + module_globals[name] = val + if name in {"NoReturn"}: + msg = ( + f"'mypy_extensions.{name}' is deprecated, " + "and will be removed in a future version. " + f"Use 'typing.{name}' or 'typing_extensions.{name}' instead" + ) + else: + assert False, f"Add deprecation message for 'mypy_extensions.{name}'" + import warnings + warnings.warn(msg, DeprecationWarning, stacklevel=3) + return val + + +def __getattr__(name: str) -> Any: + return _warn_deprecation(name, module_globals=globals()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9b753d9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["flit_core>=3.11,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "mypy_extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +readme = "README.md" +authors = [ + {name = "The mypy developers", email = "jukka.lehtosalo@iki.fi"} +] +license = "MIT" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development", +] +requires-python = ">=3.8" + +[project.urls] +Homepage = "https://github.com/python/mypy_extensions" + +[tool.flit.sdist] +include = ["tox.ini", "*/*test*.py"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index aa76bae..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 0a13ba3..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import setup - -version = '1.0.0-dev' -description = 'Type system extensions for programs checked with the mypy type checker.' -long_description = ''' -Mypy Extensions -=============== - -The "mypy_extensions" module defines extensions to the standard "typing" module -that are supported by the mypy type checker and the mypyc compiler. -'''.lstrip() - -classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Topic :: Software Development', -] - -setup( - name='mypy_extensions', - python_requires='>=3.5', - version=version, - description=description, - long_description=long_description, - author='The mypy developers', - author_email='jukka.lehtosalo@iki.fi', - url='https://github.com/python/mypy_extensions', - license='MIT License', - py_modules=['mypy_extensions'], - classifiers=classifiers, -) diff --git a/tests/testextensions.py b/tests/testextensions.py index 991c4e5..72a4a49 100644 --- a/tests/testextensions.py +++ b/tests/testextensions.py @@ -1,7 +1,10 @@ -import sys +import collections.abc import pickle +import sys import typing -from unittest import TestCase, main, skipUnless +import warnings +from contextlib import contextmanager +from unittest import TestCase, main from mypy_extensions import TypedDict, i64, i32, i16, u8 @@ -22,35 +25,36 @@ def assertNotIsSubclass(self, cls, class_or_tuple, msg=None): raise self.failureException(message) -PY36 = sys.version_info[:2] >= (3, 6) +with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) -PY36_TESTS = """ -Label = TypedDict('Label', [('label', str)]) + Label = TypedDict('Label', [('label', str)]) -class Point2D(TypedDict): - x: int - y: int + class Point2D(TypedDict): + x: int + y: int -class LabelPoint2D(Point2D, Label): ... + class LabelPoint2D(Point2D, Label): ... -class Options(TypedDict, total=False): - log_level: int - log_path: str -""" - -if PY36: - exec(PY36_TESTS) + class Options(TypedDict, total=False): + log_level: int + log_path: str class TypedDictTests(BaseTestCase): + @contextmanager + def assert_typeddict_deprecated(self): + with self.assertWarnsRegex( + DeprecationWarning, "mypy_extensions.TypedDict is deprecated" + ): + yield def test_basics_iterable_syntax(self): - Emp = TypedDict('Emp', {'name': str, 'id': int}) + with self.assert_typeddict_deprecated(): + Emp = TypedDict('Emp', {'name': str, 'id': int}) self.assertIsSubclass(Emp, dict) self.assertIsSubclass(Emp, typing.MutableMapping) - if sys.version_info[0] >= 3: - import collections.abc - self.assertNotIsSubclass(Emp, collections.abc.Sequence) + self.assertNotIsSubclass(Emp, collections.abc.Sequence) jim = Emp(name='Jim', id=1) self.assertIs(type(jim), dict) self.assertEqual(jim['name'], 'Jim') @@ -62,12 +66,11 @@ def test_basics_iterable_syntax(self): self.assertEqual(Emp.__total__, True) def test_basics_keywords_syntax(self): - Emp = TypedDict('Emp', name=str, id=int) + with self.assert_typeddict_deprecated(): + Emp = TypedDict('Emp', name=str, id=int) self.assertIsSubclass(Emp, dict) self.assertIsSubclass(Emp, typing.MutableMapping) - if sys.version_info[0] >= 3: - import collections.abc - self.assertNotIsSubclass(Emp, collections.abc.Sequence) + self.assertNotIsSubclass(Emp, collections.abc.Sequence) jim = Emp(name='Jim', id=1) # type: ignore # mypy doesn't support keyword syntax yet self.assertIs(type(jim), dict) self.assertEqual(jim['name'], 'Jim') @@ -79,7 +82,8 @@ def test_basics_keywords_syntax(self): self.assertEqual(Emp.__total__, True) def test_typeddict_errors(self): - Emp = TypedDict('Emp', {'name': str, 'id': int}) + with self.assert_typeddict_deprecated(): + Emp = TypedDict('Emp', {'name': str, 'id': int}) self.assertEqual(TypedDict.__module__, 'mypy_extensions') jim = Emp(name='Jim', id=1) with self.assertRaises(TypeError): @@ -88,14 +92,13 @@ def test_typeddict_errors(self): isinstance(jim, Emp) # type: ignore with self.assertRaises(TypeError): issubclass(dict, Emp) # type: ignore - with self.assertRaises(TypeError): + with self.assertRaises(TypeError), self.assert_typeddict_deprecated(): TypedDict('Hi', x=()) - with self.assertRaises(TypeError): + with self.assertRaises(TypeError), self.assert_typeddict_deprecated(): TypedDict('Hi', [('x', int), ('y', ())]) with self.assertRaises(TypeError): TypedDict('Hi', [('x', int)], y=int) - @skipUnless(PY36, 'Python 3.6 required') def test_py36_class_syntax_usage(self): self.assertEqual(LabelPoint2D.__name__, 'LabelPoint2D') # noqa self.assertEqual(LabelPoint2D.__module__, __name__) # noqa @@ -109,9 +112,15 @@ def test_py36_class_syntax_usage(self): other = LabelPoint2D(x=0, y=1, label='hi') # noqa self.assertEqual(other['label'], 'hi') + def test_py36_class_usage_emits_deprecations(self): + with self.assert_typeddict_deprecated(): + class Foo(TypedDict): + bar: int + def test_pickle(self): global EmpD # pickle wants to reference the class by name - EmpD = TypedDict('EmpD', name=str, id=int) + with self.assert_typeddict_deprecated(): + EmpD = TypedDict('EmpD', name=str, id=int) jane = EmpD({'name': 'jane', 'id': 37}) for proto in range(pickle.HIGHEST_PROTOCOL + 1): z = pickle.dumps(jane, proto) @@ -123,21 +132,22 @@ def test_pickle(self): self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane) def test_optional(self): - EmpD = TypedDict('EmpD', name=str, id=int) + with self.assert_typeddict_deprecated(): + EmpD = TypedDict('EmpD', name=str, id=int) self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD]) self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD]) def test_total(self): - D = TypedDict('D', {'x': int}, total=False) + with self.assert_typeddict_deprecated(): + D = TypedDict('D', {'x': int}, total=False) self.assertEqual(D(), {}) self.assertEqual(D(x=1), {'x': 1}) self.assertEqual(D.__total__, False) - if PY36: - self.assertEqual(Options(), {}) # noqa - self.assertEqual(Options(log_level=2), {'log_level': 2}) # noqa - self.assertEqual(Options.__total__, False) # noqa + self.assertEqual(Options(), {}) # noqa + self.assertEqual(Options(log_level=2), {'log_level': 2}) # noqa + self.assertEqual(Options.__total__, False) # noqa native_int_types = [i64, i32, i16, u8] @@ -181,5 +191,21 @@ def assert_same(self, x, y): assert x == y +class DeprecationTests(TestCase): + def test_no_return_deprecation(self): + del sys.modules["mypy_extensions"] + with self.assertWarnsRegex( + DeprecationWarning, "'mypy_extensions.NoReturn' is deprecated" + ): + import mypy_extensions + mypy_extensions.NoReturn + + del sys.modules["mypy_extensions"] + with self.assertWarnsRegex( + DeprecationWarning, "'mypy_extensions.NoReturn' is deprecated" + ): + from mypy_extensions import NoReturn # noqa: F401 + + if __name__ == '__main__': main() diff --git a/tox.ini b/tox.ini index 0a61ca8..3412839 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,15 @@ [tox] -minversion = 2.9.1 +minversion = 4.4.4 skip_missing_interpreters = true -envlist = py35, py36, py37, py38, py39, py310, py311 +envlist = py38, py39, py310, py311, py312, py313 [testenv] description = run the test driver with {basepython} -commands = python -m unittest discover tests +commands = python -We -m unittest discover tests [testenv:lint] description = check the code style -basepython = python3.7 +basepython = python3 deps = flake8 commands = flake8 -j0 {posargs}