diff --git a/Makefile b/Makefile index 3c86cde313..73f2d5e68c 100644 --- a/Makefile +++ b/Makefile @@ -25,13 +25,13 @@ rebuild-lockfiles: .pdm .PHONY: format ## Auto-format python source files format: .pdm - pdm run black $(sources) pdm run ruff --fix $(sources) + pdm run ruff format $(sources) .PHONY: lint ## Lint python source files lint: .pdm pdm run ruff $(sources) - pdm run black $(sources) --check --diff + pdm run ruff format --check $(sources) .PHONY: codespell ## Use Codespell to do spellchecking codespell: .pre-commit diff --git a/docs/contributing.md b/docs/contributing.md index 366c2225c8..1831b61d3b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -90,8 +90,8 @@ Run tests and linting locally to make sure everything is working as expected. ```bash # Run automated code formatting and linting make format -# Pydantic uses black and ruff -# (https://github.com/ambv/black, https://github.com/charliermarsh/ruff) +# Pydantic uses ruff, an awesome Python linter written in rust +# https://github.com/astral-sh/ruff # Run tests and linting make diff --git a/pdm.lock b/pdm.lock index 31a357c055..78a7b5bf4f 100644 --- a/pdm.lock +++ b/pdm.lock @@ -73,48 +73,6 @@ files = [ {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, ] -[[package]] -name = "black" -version = "23.3.0" -requires_python = ">=3.7" -summary = "The uncompromising code formatter." -dependencies = [ - "click>=8.0.0", - "mypy-extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=0.9.0", - "platformdirs>=2", - "tomli>=1.1.0; python_version < \"3.11\"", - "typed-ast>=1.4.2; python_version < \"3.8\" and implementation_name == \"cpython\"", - "typing-extensions>=3.10.0.0; python_version < \"3.10\"", -] -files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, -] [[package]] name = "cached-property" diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py index d7423ce85c..9cf88e3abb 100644 --- a/pydantic/_internal/_model_construction.py +++ b/pydantic/_internal/_model_construction.py @@ -367,8 +367,8 @@ def inspect_namespace( # noqa C901 ) else: raise PydanticUserError( - f"A non-annotated attribute was detected: `{var_name} = {value!r}`. All model fields require a " - f"type annotation; if `{var_name}` is not meant to be a field, you may be able to resolve this " + f'A non-annotated attribute was detected: `{var_name} = {value!r}`. All model fields require a ' + f'type annotation; if `{var_name}` is not meant to be a field, you may be able to resolve this ' f"error by annotating it as a `ClassVar` or updating `model_config['ignored_types']`.", code='model-field-missing-annotation', ) diff --git a/pydantic/color.py b/pydantic/color.py index 5aabec4981..108bb8faec 100644 --- a/pydantic/color.py +++ b/pydantic/color.py @@ -55,13 +55,13 @@ def __getitem__(self, item: Any) -> Any: r_hex_short = r'\s*(?:#|0x)?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?\s*' r_hex_long = r'\s*(?:#|0x)?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?\s*' # CSS3 RGB examples: rgb(0, 0, 0), rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 50%) -r_rgb = fr'\s*rgba?\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}(?:{_r_comma}{_r_alpha})?\s*\)\s*' +r_rgb = rf'\s*rgba?\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}(?:{_r_comma}{_r_alpha})?\s*\)\s*' # CSS3 HSL examples: hsl(270, 60%, 50%), hsla(270, 60%, 50%, 0.5), hsla(270, 60%, 50%, 50%) -r_hsl = fr'\s*hsla?\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}(?:{_r_comma}{_r_alpha})?\s*\)\s*' +r_hsl = rf'\s*hsla?\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}(?:{_r_comma}{_r_alpha})?\s*\)\s*' # CSS4 RGB examples: rgb(0 0 0), rgb(0 0 0 / 0.5), rgb(0 0 0 / 50%), rgba(0 0 0 / 50%) -r_rgb_v4_style = fr'\s*rgba?\(\s*{_r_255}\s+{_r_255}\s+{_r_255}(?:\s*/\s*{_r_alpha})?\s*\)\s*' +r_rgb_v4_style = rf'\s*rgba?\(\s*{_r_255}\s+{_r_255}\s+{_r_255}(?:\s*/\s*{_r_alpha})?\s*\)\s*' # CSS4 HSL examples: hsl(270 60% 50%), hsl(270 60% 50% / 0.5), hsl(270 60% 50% / 50%), hsla(270 60% 50% / 50%) -r_hsl_v4_style = fr'\s*hsla?\(\s*{_r_h}\s+{_r_sl}\s+{_r_sl}(?:\s*/\s*{_r_alpha})?\s*\)\s*' +r_hsl_v4_style = rf'\s*hsla?\(\s*{_r_h}\s+{_r_sl}\s+{_r_sl}(?:\s*/\s*{_r_alpha})?\s*\)\s*' # colors where the two hex characters are the same, if all colors match this the short version of hex colors can be used repeat_colors = {int(c * 2, 16) for c in '0123456789abcdef'} diff --git a/pydantic/deprecated/class_validators.py b/pydantic/deprecated/class_validators.py index dc65e75315..43db6e2658 100644 --- a/pydantic/deprecated/class_validators.py +++ b/pydantic/deprecated/class_validators.py @@ -114,13 +114,13 @@ def validator( fields = tuple((__field, *fields)) if isinstance(fields[0], FunctionType): raise PydanticUserError( - "`@validator` should be used with fields and keyword arguments, not bare. " + '`@validator` should be used with fields and keyword arguments, not bare. ' "E.g. usage should be `@validator('', ...)`", code='validator-no-fields', ) elif not all(isinstance(field, str) for field in fields): raise PydanticUserError( - "`@validator` fields should be passed as separate string args. " + '`@validator` fields should be passed as separate string args. ' "E.g. usage should be `@validator('', '', ...)`", code='validator-invalid-fields', ) @@ -162,7 +162,10 @@ def root_validator( # which means you need to specify `skip_on_failure=True` skip_on_failure: Literal[True], allow_reuse: bool = ..., -) -> Callable[[_V1RootValidatorFunctionType], _V1RootValidatorFunctionType,]: +) -> Callable[ + [_V1RootValidatorFunctionType], + _V1RootValidatorFunctionType, +]: ... @@ -173,7 +176,10 @@ def root_validator( # `skip_on_failure`, in fact it is not allowed as an argument! pre: Literal[True], allow_reuse: bool = ..., -) -> Callable[[_V1RootValidatorFunctionType], _V1RootValidatorFunctionType,]: +) -> Callable[ + [_V1RootValidatorFunctionType], + _V1RootValidatorFunctionType, +]: ... @@ -185,7 +191,10 @@ def root_validator( pre: Literal[False], skip_on_failure: Literal[True], allow_reuse: bool = ..., -) -> Callable[[_V1RootValidatorFunctionType], _V1RootValidatorFunctionType,]: +) -> Callable[ + [_V1RootValidatorFunctionType], + _V1RootValidatorFunctionType, +]: ... diff --git a/pydantic/main.py b/pydantic/main.py index cef995a1f8..924132f3f8 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -1110,7 +1110,7 @@ def parse_file( # noqa: D102 @classmethod @typing_extensions.deprecated( - "The `from_orm` method is deprecated; set " + 'The `from_orm` method is deprecated; set ' "`model_config['from_attributes']=True` and use `model_validate` instead.", category=PydanticDeprecatedSince20, ) diff --git a/pydantic/networks.py b/pydantic/networks.py index 7dc1e5ac27..e9f25ea373 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -636,7 +636,7 @@ def _validate(cls, __input_value: NetworkType) -> IPv4Network | IPv6Network: def _build_pretty_email_regex() -> re.Pattern[str]: name_chars = r'[\w!#$%&\'*+\-/=?^_`{|}~]' - unquoted_name_group = fr'((?:{name_chars}+\s+)*{name_chars}+)' + unquoted_name_group = rf'((?:{name_chars}+\s+)*{name_chars}+)' quoted_name_group = r'"((?:[^"]|\")+)"' email_group = r'<\s*(.+)\s*>' return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*') diff --git a/pyproject.toml b/pyproject.toml index 8e65d4e9fc..ef46d6af3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,14 +84,12 @@ docs = [ "pyupgrade", "mike @ git+https://github.com/jimporter/mike.git", "mkdocs-embed-external-markdown>=2.3.0", - "black>=23.3.0", "pytest-examples>=0.0.10", "pydantic-settings>=2.0b1", "pydantic-extra-types @ git+https://github.com/pydantic/pydantic-extra-types.git@main" ] linting = [ - "black", - "ruff", + "ruff==0.1.3", "mypy~=1.1.1", ] testing = [ @@ -176,8 +174,7 @@ unconfirmed_label = 'pending' [tool.ruff] line-length = 120 extend-select = ['Q', 'RUF100', 'C90', 'UP', 'I', 'D', 'T'] -# E501 is handled by black -extend-ignore = ['D105', 'D107', 'D205', 'D415', 'E501'] +extend-ignore = ['D105', 'D107', 'D205', 'D415'] flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'} mccabe = { max-complexity = 14 } isort = { known-first-party = ['pydantic', 'tests'] } @@ -190,6 +187,9 @@ extend-exclude = ['pydantic/v1', 'tests/mypy/outputs'] "tests/benchmarks/test_fastapi_startup_generics.py" = ['UP006', 'UP007'] "tests/benchmarks/test_fastapi_startup_simple.py" = ['UP006', 'UP007'] +[tool.ruff.format] +quote-style = 'single' + [tool.ruff.pydocstyle] convention = "google" @@ -228,19 +228,6 @@ source = [ 'D:\a\pydantic\pydantic\pydantic', ] -[tool.black] -color = true -line-length = 120 -target-version = ['py310'] -skip-string-normalization = true -exclude = ''' -( - /( - pydantic/v1 - | tests/mypy/outputs - )/ -) -''' [tool.pyright] include = ['pydantic'] diff --git a/tests/check_usage_docs.py b/tests/check_usage_docs.py index fc28625b54..6eb57aed64 100644 --- a/tests/check_usage_docs.py +++ b/tests/check_usage_docs.py @@ -10,7 +10,7 @@ version_file = PYDANTIC_DIR / 'version.py' -version = re.search(br"VERSION = '(.*)'", version_file.read_bytes()).group(1) +version = re.search(rb"VERSION = '(.*)'", version_file.read_bytes()).group(1) version_major_minor = b'.'.join(version.split(b'.')[:2]) expected_base = b'https://docs.pydantic.dev/' + version_major_minor + b'/' @@ -30,7 +30,7 @@ def sub(m: re.Match) -> bytes: else: return m.group(0) - b = re.sub(br'(""" *usage.docs: *)(https://.+?/.+?/)', sub, b, flags=re.I) + b = re.sub(rb'(""" *usage.docs: *)(https://.+?/.+?/)', sub, b, flags=re.I) if changed: error_count += changed path.write_bytes(b) diff --git a/tests/test_discriminated_union.py b/tests/test_discriminated_union.py index 26c044fbf3..eec6fbb344 100644 --- a/tests/test_discriminated_union.py +++ b/tests/test_discriminated_union.py @@ -361,7 +361,7 @@ class Top(BaseModel): 'ctx': {'discriminator': "'m'", 'expected_tags': '1, 2', 'tag': '3'}, 'input': {'m': 3}, 'loc': ('sub',), - 'msg': "Input tag '3' found using 'm' does not match any of the expected " "tags: 1, 2", + 'msg': "Input tag '3' found using 'm' does not match any of the expected " 'tags: 1, 2', 'type': 'union_tag_invalid', } ] @@ -911,7 +911,7 @@ def test_wrap_function_schema() -> None: def test_plain_function_schema_is_invalid() -> None: with pytest.raises( TypeError, - match="'function-plain' is not a valid discriminated union variant; " "should be a `BaseModel` or `dataclass`", + match="'function-plain' is not a valid discriminated union variant; " 'should be a `BaseModel` or `dataclass`', ): apply_discriminator( core_schema.union_schema( diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 2a306a86a7..6a990ff989 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -1127,7 +1127,7 @@ class C(A): TypeError, match=( "Field 'integer' defined on a base class was overridden by a non-annotated attribute. " - "All field definitions, including overrides, require a type annotation." + 'All field definitions, including overrides, require a type annotation.' ), ): @@ -1197,8 +1197,8 @@ def test_unable_to_infer(): with pytest.raises( errors.PydanticUserError, match=re.escape( - "A non-annotated attribute was detected: `x = None`. All model fields require a type annotation; " - "if `x` is not meant to be a field, you may be able to resolve this error by annotating it as a " + 'A non-annotated attribute was detected: `x = None`. All model fields require a type annotation; ' + 'if `x` is not meant to be a field, you may be able to resolve this error by annotating it as a ' "`ClassVar` or updating `model_config['ignored_types']`" ), ): @@ -1541,7 +1541,7 @@ class Model(BaseModel): model_config = dict(arbitrary_types_allowed=True) - assert re.search(fr'\(annotation={re.escape(expected)},', str(Model.model_fields)) + assert re.search(rf'\(annotation={re.escape(expected)},', str(Model.model_fields)) def test_any_none(): diff --git a/tests/test_generics.py b/tests/test_generics.py index 6420348cf2..67586a6d4a 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -268,7 +268,7 @@ class Result(BaseModel): assert str(exc_info.value) == ( ".Result'> cannot be " - "parametrized because it does not inherit from typing.Generic" + 'parametrized because it does not inherit from typing.Generic' ) diff --git a/tests/test_main.py b/tests/test_main.py index 82367b41c2..d2d9c85034 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1511,8 +1511,8 @@ def test_untyped_fields_warning(): with pytest.raises( PydanticUserError, match=re.escape( - "A non-annotated attribute was detected: `x = 1`. All model fields require a type annotation; " - "if `x` is not meant to be a field, you may be able to resolve this error by annotating it " + 'A non-annotated attribute was detected: `x = 1`. All model fields require a type annotation; ' + 'if `x` is not meant to be a field, you may be able to resolve this error by annotating it ' "as a `ClassVar` or updating `model_config['ignored_types']`." ), ): diff --git a/tests/test_networks.py b/tests/test_networks.py index 852c7763d3..dc3104d776 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -805,7 +805,6 @@ def test_address_valid(value, name, email): ('\u0020@example.com', None), ('\u001f@example.com', None), ('"@example.com', None), - ('\"@example.com', None), (',@example.com', None), ('foobar ', None), ('foobar >', None), diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 771026a1f8..25bda29f0c 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -284,7 +284,7 @@ class Model(BaseModel): assert Model.model_validate_json('{"a": 2}', context={'c': 2}).model_dump() == {'a': 2} # insert_assert(log) assert log == [ - 'json enter input={"a": 2} kwargs={\'strict\': None, \'context\': {\'c\': 2}}', + "json enter input={\"a\": 2} kwargs={'strict': None, 'context': {'c': 2}}", 'json success result=a=2', ] log.clear() diff --git a/tests/test_private_attributes.py b/tests/test_private_attributes.py index 7f51b6f4b6..9241163fd6 100644 --- a/tests/test_private_attributes.py +++ b/tests/test_private_attributes.py @@ -289,7 +289,7 @@ class Model(ParentAModel, ParentBModel): def test_private_attributes_not_dunder() -> None: with pytest.raises( NameError, - match="Private attributes must not use dunder names;" " use a single underscore prefix instead of '__foo__'.", + match='Private attributes must not use dunder names;' " use a single underscore prefix instead of '__foo__'.", ): class MyModel(BaseModel): diff --git a/tests/test_types.py b/tests/test_types.py index 417d3e4b2d..02ff55adfc 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -4852,7 +4852,7 @@ def test_union_uuid_str_left_to_right(): # in smart mode JSON and python are currently validated differently in this # case, because in Python this is a str but in JSON a str is also a UUID - assert TypeAdapter(IdOrSlug).validate_json('\"f4fe10b4-e0c8-4232-ba26-4acd491c2414\"') == UUID( + assert TypeAdapter(IdOrSlug).validate_json('"f4fe10b4-e0c8-4232-ba26-4acd491c2414"') == UUID( 'f4fe10b4-e0c8-4232-ba26-4acd491c2414' ) assert ( @@ -4863,7 +4863,7 @@ def test_union_uuid_str_left_to_right(): IdOrSlugLTR = Annotated[Union[UUID, str], Field(union_mode='left_to_right')] # in left to right mode both JSON and python are validated as UUID - assert TypeAdapter(IdOrSlugLTR).validate_json('\"f4fe10b4-e0c8-4232-ba26-4acd491c2414\"') == UUID( + assert TypeAdapter(IdOrSlugLTR).validate_json('"f4fe10b4-e0c8-4232-ba26-4acd491c2414"') == UUID( 'f4fe10b4-e0c8-4232-ba26-4acd491c2414' ) assert TypeAdapter(IdOrSlugLTR).validate_python('f4fe10b4-e0c8-4232-ba26-4acd491c2414') == UUID(