diff --git a/.circleci/config.yml b/.circleci/config.yml index 1690bfccc..1957d339f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,4 +39,4 @@ workflows: - test_linux: matrix: parameters: - py_version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + py_version: ["3.8", "3.9", "3.10", "3.11"] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..19920f0a9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + + +## Motivation + +(Write your motivation for proposed changes here.) + +### Have you read the [Contributing Guidelines on pull requests](https://github.com/omry/omegaconf/blob/master/CONTRIBUTING.md)? + +Yes/No + +## Test Plan + +(How should this PR be tested? Do you require special setup to run the test or repro the fixed bug?) + +## Fixes + +What issue does this PR fix? Use https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue to link this PR to a corresponding issue. + +Fixes # + +## Related PRs + +(Is this PR part of a group of changes? Link the other relevant PRs.) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19cad4e16..3bec811c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,26 @@ +default_language_version: + python: python3.8 + +# Hook versions should match those in requirements/dev.txt repos: - - repo: https://github.com/timothycrosley/isort - rev: 5.0.9 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 23.7.0 hooks: - id: black - language_version: python3.8 - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 hooks: - id: flake8 - additional_dependencies: [-e, 'git+https://github.com/pycqa/pyflakes.git@1911c20#egg=pyflakes'] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v1.4.1 hooks: - id: mypy args: [--strict] - additional_dependencies: ['pytest'] + additional_dependencies: ['attrs', 'pytest'] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c1dae853c..f086a78fa 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -16,7 +16,7 @@ formats: # Optionally set the version of Python and requirements required to build your docs python: - version: 3.7 + version: 3.8 install: - method: pip path: . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b165a5c68..ffda44b09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,11 +15,11 @@ Optionally install commit hooks: `pre-commit install` pre-commit will verify your code lints cleanly when you commit. You can use `git commit -n` to skip the pre-commit hook for a specific commit. ##### Mac -OmegaConf is compatible with Python 3.6.4 and newer. Unfortunately Mac comes with older versions. +OmegaConf is compatible with Python 3.8 and newer. One way to install multiple Python versions on Mac to to use pyenv. The instructions [here](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/MAC_SETUP.md) -will provide full details. It shows how to use pyenv on mac to install multiple versions of Python and have +will provide full details. It shows how to use pyenv on Mac to install multiple versions of Python and have pyenv make specific versions available in specific directories automatically. This plays well with Conda, which supports a single Python version. Pyenv will provide the versions not installed by Conda (which are used when running nox). @@ -31,24 +31,16 @@ Run all CI tests with nox: ``` $ nox -l Sessions defined in /home/omry/dev/omegaconf/noxfile.py: -* omegaconf-3.6 -* omegaconf-3.7 * omegaconf-3.8 * omegaconf-3.9 * omegaconf-3.10 * docs -* coverage-3.6 -* coverage-3.7 * coverage-3.8 * coverage-3.9 * coverage-3.10 -* lint-3.6 -* lint-3.7 * lint-3.8 * lint-3.9 * lint-3.10 -* test_jupyter_notebook-3.6 -* test_jupyter_notebook-3.7 * test_jupyter_notebook-3.8 * test_jupyter_notebook-3.9 * test_jupyter_notebook-3.10 diff --git a/docs/source/structured_config.rst b/docs/source/structured_config.rst index 8a3f72349..92207edf3 100644 --- a/docs/source/structured_config.rst +++ b/docs/source/structured_config.rst @@ -22,10 +22,8 @@ Structured Configs Structured configs are used to create OmegaConf configuration object with runtime type safety. In addition, they can be used with tools like mypy or your IDE for static type checking. -Two types of structures classes are supported: dataclasses and attr classes. - -- `dataclasses `_ are standard as of Python 3.7 or newer and are available in Python 3.6 via the `dataclasses` pip package. -- `attrs `_ Offset slightly cleaner syntax in some cases but depends on the attrs pip package. +Two types of structures classes are supported: `dataclasses `_ and `attrs `_ classes +(that offers slightly cleaner syntax in some cases but depends on the attrs pip package). This documentation will use dataclasses, but you can use the annotation ``@attr.s(auto_attribs=True)`` from attrs instead of ``@dataclass``. @@ -440,10 +438,6 @@ OmegaConf supports field modifiers such as ``MISSING`` and ``Optional``. >>> conf: Modifiers = OmegaConf.structured(Modifiers) -Note for Python3.6 users: :ref:`pickling ` -structured configs with complex type annotations, such as dict-of-list or -list-of-optional, is not supported. - Mandatory missing values ++++++++++++++++++++++++ diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 3ead89099..7f63b19a7 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -27,7 +27,7 @@ Just pip install:: pip install omegaconf -OmegaConf requires Python 3.6 and newer. +OmegaConf requires Python 3.8 or newer. .. _creating: @@ -316,12 +316,6 @@ Note that the saved file may be incompatible across different versions of OmegaC ... loaded = pickle.load(fp) ... assert conf == loaded -Note for Python3.6 users: due to limitations in pickling support, -:ref:`structured configs ` with complex type hints (such as -:ref:`nested container types ` or -:ref:`containers with optional element types `) cannot -be pickled using Python3.6. - .. _interpolation: diff --git a/news/1109.api_change b/news/1109.api_change new file mode 100644 index 000000000..ce3d91e39 --- /dev/null +++ b/news/1109.api_change @@ -0,0 +1 @@ +Python 3.6 and 3.7 are not supported anymore: OmegaConf now requires Python 3.8+ diff --git a/noxfile.py b/noxfile.py index 3201f1812..db547cb30 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,7 +4,7 @@ import nox from nox import Session -DEFAULT_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] +DEFAULT_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11"] PYTHON_VERSIONS = os.environ.get( "NOX_PYTHON_VERSIONS", ",".join(DEFAULT_PYTHON_VERSIONS) @@ -63,7 +63,7 @@ def version_string_to_tuple(version: str) -> Tuple[int, ...]: return tuple(map(int, version.split("."))) -@nox.session(python=[v for v in PYTHON_VERSIONS if version_string_to_tuple(v) >= (3, 7)]) # type: ignore +@nox.session(python=PYTHON_VERSIONS) # type: ignore def lint(session: Session) -> None: deps(session, editable_install=True) session.run( diff --git a/omegaconf/_utils.py b/omegaconf/_utils.py index 3452f48ca..a2582f7e1 100644 --- a/omegaconf/_utils.py +++ b/omegaconf/_utils.py @@ -6,20 +6,9 @@ import sys import types import warnings -from contextlib import contextmanager from enum import Enum from textwrap import dedent -from typing import ( - Any, - Dict, - Iterator, - List, - Optional, - Tuple, - Type, - Union, - get_type_hints, -) +from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_type_hints import yaml @@ -635,32 +624,22 @@ def is_dict_annotation(type_: Any) -> bool: origin = getattr(type_, "__origin__", None) # type_dict is a bit hard to detect. # this support is tentative, if it eventually causes issues in other areas it may be dropped. - if sys.version_info < (3, 7, 0): # pragma: no cover - typed_dict = hasattr(type_, "__base__") and type_.__base__ == Dict - return origin is Dict or type_ is Dict or typed_dict - else: # pragma: no cover - typed_dict = hasattr(type_, "__base__") and type_.__base__ == dict - return origin is dict or typed_dict + typed_dict = hasattr(type_, "__base__") and type_.__base__ == dict + return origin is dict or typed_dict def is_list_annotation(type_: Any) -> bool: if type_ in (list, List): return True origin = getattr(type_, "__origin__", None) - if sys.version_info < (3, 7, 0): - return origin is List or type_ is List # pragma: no cover - else: - return origin is list # pragma: no cover + return origin is list def is_tuple_annotation(type_: Any) -> bool: if type_ in (tuple, Tuple): return True origin = getattr(type_, "__origin__", None) - if sys.version_info < (3, 7, 0): - return origin is Tuple or type_ is Tuple # pragma: no cover - else: - return origin is tuple # pragma: no cover + return origin is tuple def is_supported_union_annotation(obj: Any) -> bool: @@ -1032,10 +1011,3 @@ def split_key(key: str) -> List[str]: tokens += [dot_key if dot_key else bracket_key for dot_key, bracket_key in others] return tokens - - -# Similar to Python 3.7+'s `contextlib.nullcontext` (which should be used instead, -# once support for Python 3.6 is dropped). -@contextmanager -def nullcontext(enter_result: Any = None) -> Iterator[Any]: - yield enter_result diff --git a/omegaconf/basecontainer.py b/omegaconf/basecontainer.py index 156b1ca30..a83907f8e 100644 --- a/omegaconf/basecontainer.py +++ b/omegaconf/basecontainer.py @@ -45,7 +45,6 @@ InterpolationResolutionError, KeyValidationError, MissingMandatoryValue, - OmegaConfBaseException, ReadonlyConfigError, ValidationError, ) @@ -133,12 +132,6 @@ def __getstate__(self) -> Dict[str, Any]: dict_copy["_metadata"].ref_type = List else: assert False - if sys.version_info < (3, 7): # pragma: no cover - element_type = self._metadata.element_type - if is_union_annotation(element_type): - raise OmegaConfBaseException( - "Serializing structured configs with `Union` element type requires python >= 3.7" - ) return dict_copy # Support pickle diff --git a/omegaconf/omegaconf.py b/omegaconf/omegaconf.py index 041602879..8b130895b 100644 --- a/omegaconf/omegaconf.py +++ b/omegaconf/omegaconf.py @@ -7,7 +7,7 @@ import sys import warnings from collections import defaultdict -from contextlib import contextmanager +from contextlib import contextmanager, nullcontext from enum import Enum from textwrap import dedent from typing import ( @@ -49,7 +49,6 @@ is_structured_config, is_tuple_annotation, is_union_annotation, - nullcontext, split_key, type_str, ) diff --git a/omegaconf/version.py b/omegaconf/version.py index f73c6b765..7ea7f3388 100644 --- a/omegaconf/version.py +++ b/omegaconf/version.py @@ -1,13 +1,14 @@ import sys # pragma: no cover -__version__ = "2.4.0.dev0" +__version__ = "2.4.0.dev1" -msg = """OmegaConf 2.0 and above is compatible with Python 3.6 and newer. +msg = """OmegaConf 2.4 and above is compatible with Python 3.8 and newer. You have the following options: -1. Upgrade to Python 3.6 or newer. - This is highly recommended. new features will not be added to OmegaConf 1.4. -2. Continue using OmegaConf 1.4: - You can pip install 'OmegaConf<1.5' to do that. +1. Upgrade to Python 3.8 or newer. + This is highly recommended. new features will not be added to OmegaConf 2.3. +2. Continue using OmegaConf 2.3: + You can pip install 'OmegaConf<2.4' to do that. """ -if sys.version_info < (3, 6): + +if sys.version_info < (3, 8): raise ImportError(msg) # pragma: no cover diff --git a/requirements/dev.txt b/requirements/dev.txt index eedeeb1aa..6f7b94444 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,12 +1,12 @@ -r base.txt -r docs.txt attrs -black +black==23.7.0 build coveralls -flake8>=4 -isort~=5.0 -mypy +flake8==6.0.0 +isort==5.12.0 +mypy==1.4.1 nox pre-commit pyflakes @@ -15,6 +15,6 @@ pytest-benchmark pytest-lazy-fixture pytest-mock towncrier +types-setuptools # makes mypy happy twine pydevd - diff --git a/setup.cfg b/setup.cfg index c8b0a030b..fa149a690 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ test=pytest [mypy] -python_version = 3.7 +python_version = 3.8 mypy_path=.stubs exclude = build/ diff --git a/setup.py b/setup.py index 966431c6c..3962a476e 100644 --- a/setup.py +++ b/setup.py @@ -59,10 +59,8 @@ "pydevd_plugins", "pydevd_plugins.extensions", ], - python_requires=">=3.6", + python_requires=">=3.8", classifiers=[ - "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", diff --git a/tests/examples/dataclass_postponed_annotations.py b/tests/examples/dataclass_postponed_annotations.py index ada14efd7..eddb97dfb 100644 --- a/tests/examples/dataclass_postponed_annotations.py +++ b/tests/examples/dataclass_postponed_annotations.py @@ -1,6 +1,6 @@ # `from __future__` has to be the very first thing in a module # otherwise a syntax error is raised -from __future__ import annotations # noqa # Python 3.6 linters complain +from __future__ import annotations from dataclasses import dataclass, fields from enum import Enum diff --git a/tests/examples/test_postponed_annotations.py b/tests/examples/test_postponed_annotations.py index 750a3babc..716c27629 100644 --- a/tests/examples/test_postponed_annotations.py +++ b/tests/examples/test_postponed_annotations.py @@ -1,9 +1,3 @@ -import sys - -from pytest import mark - - -@mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7") def test_simple_types_class_postponed() -> None: # import from a module which has `from __future__ import annotations` from tests.examples.dataclass_postponed_annotations import simple_types_class @@ -11,7 +5,6 @@ def test_simple_types_class_postponed() -> None: simple_types_class() -@mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7") def test_conversions_postponed() -> None: # import from a module which has `from __future__ import annotations` from tests.examples.dataclass_postponed_annotations import conversions diff --git a/tests/test_base_config.py b/tests/test_base_config.py index 274a8b573..6129120c1 100644 --- a/tests/test_base_config.py +++ b/tests/test_base_config.py @@ -1,4 +1,5 @@ import copy +from contextlib import nullcontext from typing import Any, Dict, List, Optional, Union from pytest import mark, param, raises @@ -19,7 +20,7 @@ open_dict, read_write, ) -from omegaconf._utils import _ensure_container, nullcontext +from omegaconf._utils import _ensure_container from omegaconf.errors import ConfigAttributeError, ConfigKeyError, MissingMandatoryValue from tests import ( ConcretePlugin, diff --git a/tests/test_basic_ops_list.py b/tests/test_basic_ops_list.py index 0ff4f5f9e..e901efc5c 100644 --- a/tests/test_basic_ops_list.py +++ b/tests/test_basic_ops_list.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import re +from contextlib import nullcontext from pathlib import Path from textwrap import dedent from typing import Any, Callable, List, MutableSequence, Optional, Union @@ -8,7 +9,7 @@ from pytest import mark, param, raises from omegaconf import MISSING, AnyNode, DictConfig, ListConfig, OmegaConf, flag_override -from omegaconf._utils import _ensure_container, nullcontext +from omegaconf._utils import _ensure_container from omegaconf.base import Node from omegaconf.errors import ( ConfigTypeError, diff --git a/tests/test_grammar.py b/tests/test_grammar.py index d21d6d4a1..f4ec78989 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -2,6 +2,7 @@ import re import threading import time +from contextlib import nullcontext from typing import Any, Callable, List, Optional, Set, Tuple from pytest import mark, param, raises, warns @@ -16,7 +17,6 @@ grammar_parser, grammar_visitor, ) -from omegaconf._utils import nullcontext from omegaconf.errors import ( GrammarParseError, InterpolationKeyError, diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 141e1b3d0..5e01a3a8b 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -1,7 +1,6 @@ import copy import functools import re -import sys from enum import Enum from functools import partial from pathlib import Path @@ -105,10 +104,6 @@ def _build_union(content: Any) -> UnionNode: _build_union, True, True, - marks=mark.skipif( - sys.version_info < (3, 7), - reason="python3.6 treats Union[int, bool] as equivalent to Union[int]", - ), id="union-bool", ), param(_build_union, None, None, id="union-none"), diff --git a/tests/test_omegaconf.py b/tests/test_omegaconf.py index 1053a21cf..1097eb5af 100644 --- a/tests/test_omegaconf.py +++ b/tests/test_omegaconf.py @@ -1,5 +1,6 @@ import pathlib import platform +from contextlib import nullcontext from pathlib import Path from typing import Any, Union @@ -20,7 +21,7 @@ StringNode, UnionNode, ) -from omegaconf._utils import _is_none, nullcontext +from omegaconf._utils import _is_none from omegaconf.errors import ( ConfigKeyError, InterpolationKeyError, diff --git a/tests/test_serialization.py b/tests/test_serialization.py index a78a99f80..12d7b482d 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -4,8 +4,6 @@ import os import pathlib import pickle -import re -import sys import tempfile from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Type, Union @@ -15,7 +13,6 @@ from omegaconf import MISSING, DictConfig, ListConfig, Node, OmegaConf, UnionNode from omegaconf._utils import get_type_hint from omegaconf.base import Box -from omegaconf.errors import OmegaConfBaseException from tests import ( Color, NestedContainers, @@ -232,7 +229,6 @@ def test_load_empty_file(tmpdir: str) -> None: str, True, Optional[Dict[str, int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="opt_dict", ), param( @@ -242,7 +238,6 @@ def test_load_empty_file(tmpdir: str) -> None: str, False, Dict[str, Optional[int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="dict_opt", ), param( @@ -252,7 +247,6 @@ def test_load_empty_file(tmpdir: str) -> None: str, True, Optional[List[int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="opt_list", ), param( @@ -262,7 +256,6 @@ def test_load_empty_file(tmpdir: str) -> None: str, False, List[Optional[int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="list_opt", ), param( @@ -293,7 +286,6 @@ def test_load_empty_file(tmpdir: str) -> None: str, False, Dict[str, Dict[str, int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="dict-of-dict", ), param( @@ -303,7 +295,6 @@ def test_load_empty_file(tmpdir: str) -> None: int, False, List[List[int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="list-of-list", ), param( @@ -313,7 +304,6 @@ def test_load_empty_file(tmpdir: str) -> None: str, False, Dict[str, List[int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="dict-of-list", ), param( @@ -323,7 +313,6 @@ def test_load_empty_file(tmpdir: str) -> None: int, False, List[Dict[str, int]], - marks=mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7"), id="list-of-dict", ), ], @@ -357,7 +346,6 @@ def get_node(cfg: Any, key: Optional[str]) -> Any: assert get_node(cfg2, node)._metadata.key_type == key_type -@mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or newer") @mark.parametrize("key", ["ubf", "oubf"]) def test_pickle_union_node(key: str) -> None: cfg = OmegaConf.structured(UnionAnnotations) @@ -416,18 +404,6 @@ def test_pickle_backward_compatibility(version: str) -> None: assert cfg == OmegaConf.create({"a": [{"b": 10}]}) -@mark.skipif(sys.version_info >= (3, 7), reason="requires python3.6") -def test_python36_pickle_optional() -> None: - cfg = OmegaConf.structured(SubscriptedDictOpt) - with raises( - OmegaConfBaseException, - match=re.escape( - "Serializing structured configs with `Union` element type requires python >= 3.7" - ), - ): - pickle.dumps(cfg) - - @mark.parametrize( "copy_fn", [ @@ -442,9 +418,6 @@ def test_python36_pickle_optional() -> None: param( UnionNode(10.0, Union[float, bool]), lambda cfg: cfg._value(), - marks=mark.skipif( - sys.version_info < (3, 7), reason="requires python3.7 or newer" - ), id="union", ), param(DictConfig({"foo": "bar"}), lambda cfg: cfg._get_node("foo"), id="dict"), diff --git a/tests/test_utils.py b/tests/test_utils.py index 3be66d96e..442969d3d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -27,7 +27,6 @@ is_supported_union_annotation, is_tuple_annotation, is_union_annotation, - nullcontext, split_key, ) from omegaconf.errors import UnsupportedValueType, ValidationError @@ -682,10 +681,7 @@ def test_type_str_ellipsis() -> None: (Union[float, bool, None], "Optional[Union[float, bool]]"), (Union[float, bool, NoneType], "Optional[Union[float, bool]]"), (object, "object"), - ( - Optional[object], # python3.6 treats `Optional[object]` as `object` - "Optional[object]" if sys.version_info >= (3, 7) else "object", - ), + (Optional[object], "Optional[object]"), ], ) def test_type_str_nonetype(type_: Any, expected: str) -> None: @@ -1093,15 +1089,6 @@ def test_split_key(key: str, expected: List[str]) -> None: assert split_key(key) == expected -def test_nullcontext() -> None: - with nullcontext() as x: - assert x is None - - obj = object() - with nullcontext(obj) as x: - assert x is obj - - @mark.parametrize("is_optional", [True, False]) @mark.parametrize( "fac",