Repository: samuelcolvin/pydantic (python)

Issue:
subclasses of bytes converted to bytes
See https://github.com/duo-labs/py_webauthn/issues/113#issuecomment-1017816575

In short (I think) cython is converting subclasses of bytes to raw bytes in here:

https://github.com/samuelcolvin/pydantic/blob/9d631a3429a66f30742c1a52c94ac18ec6ba848d/pydantic/validators.py#L79

Fix should be as simple as changing the type hint.

Pull request description:
Prevent subclasses of bytes being converted to bytes
fix #3706

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [ ] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3acffa71c..7a42e0820 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,7 +4,7 @@ updates:
 - package-ecosystem: pip
   directory: /
   schedule:
-    interval: weekly
+    interval: monthly
 
 - package-ecosystem: github-actions
   directory: /
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cc81d4bfc..595c863d2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,10 +13,10 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: set up python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.9
 
@@ -40,10 +40,10 @@ jobs:
   docs-build:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: set up python
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v3
       with:
         python-version: 3.8
 
@@ -71,10 +71,10 @@ jobs:
       OS: ubuntu
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: set up python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v3
       with:
         python-version: ${{ matrix.python-version }}
 
@@ -140,10 +140,10 @@ jobs:
 
     runs-on: ${{ matrix.os }}-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: set up python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v3
       with:
         python-version: ${{ matrix.python-version }}
 
@@ -173,10 +173,10 @@ jobs:
         mypy-version: ['0.910', '0.920', '0.921']
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: set up python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v3
       with:
         python-version: '3.10'
 
@@ -210,9 +210,9 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
-      - uses: actions/setup-python@v1
+      - uses: actions/setup-python@v3
         with:
           python-version: '3.8'
 
@@ -239,13 +239,16 @@ jobs:
     name: test fastAPI
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: set up python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v3
       with:
         python-version: '3.10'
 
+    - name: upgrade pip
+      run: pip install -U pip setuptools wheel
+
     - name: install
       run: make install-testing
 
@@ -259,10 +262,10 @@ jobs:
       BENCHMARK_REPEATS: 1
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: set up python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v3
       with:
         python-version: '3.8'
 
@@ -292,10 +295,10 @@ jobs:
 
     runs-on: ${{ matrix.os }}-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: set up python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v3
       with:
         python-version: '3.8'
 
@@ -343,10 +346,10 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: set up python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v3
         with:
           python-version: '3.8'
 
diff --git a/.github/workflows/combine-dependabot.yml b/.github/workflows/combine-dependabot.yml
new file mode 100644
index 000000000..1b141a75f
--- /dev/null
+++ b/.github/workflows/combine-dependabot.yml
@@ -0,0 +1,137 @@
+# from https://github.com/hrvey/combine-prs-workflow/blob/master/combine-prs.yml
+name: 'Combine Dependabot PRs'
+
+on:
+  workflow_dispatch:
+    inputs:
+      branchPrefix:
+        description: 'Branch prefix to find combinable PRs based on'
+        required: true
+        default: 'dependabot/'
+      mustBeGreen:
+        description: 'Only combine PRs that are green'
+        required: true
+        default: true
+      combineBranchName:
+        description: 'Name of the branch to combine PRs into'
+        required: true
+        default: 'combine-dependabot-bumps'
+      ignoreLabel:
+        description: 'Exclude PRs with this label'
+        required: true
+        default: 'nocombine'
+
+jobs:
+  combine-prs:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/github-script@v6
+        id: fetch-branch-names
+        name: Fetch branch names
+        with:
+          github-token: ${{secrets.GITHUB_TOKEN}}
+          script: |
+            const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
+              owner: context.repo.owner,
+              repo: context.repo.repo
+            });
+            branches = [];
+            prs = [];
+            base_branch = null;
+            for (const pull of pulls) {
+              const branch = pull['head']['ref'];
+              console.log('Pull for branch: ' + branch);
+              if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
+                console.log('Branch matched: ' + branch);
+                statusOK = true;
+                if(${{ github.event.inputs.mustBeGreen }}) {
+                  console.log('Checking green status: ' + branch);
+                  const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{ref}/status', {
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    ref: branch
+                  });
+                  if(statuses.length > 0) {
+                    const latest_status = statuses[0]['state'];
+                    console.log('Validating status: ' + latest_status);
+                    if(latest_status != 'success') {
+                      console.log('Discarding ' + branch + ' with status ' + latest_status);
+                      statusOK = false;
+                    }
+                  }
+                }
+                console.log('Checking labels: ' + branch);
+                const labels = pull['labels'];
+                for(const label of labels) {
+                  const labelName = label['name'];
+                  console.log('Checking label: ' + labelName);
+                  if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
+                    console.log('Discarding ' + branch + ' with label ' + labelName);
+                    statusOK = false;
+                  }
+                }
+                if (statusOK) {
+                  console.log('Adding branch to array: ' + branch);
+                  branches.push(branch);
+                  prs.push('#' + pull['number'] + ' ' + pull['title']);
+                  base_branch = pull['base']['ref'];
+                }
+              }
+            }
+            if (branches.length == 0) {
+              core.setFailed('No PRs/branches matched criteria');
+              return;
+            }
+            core.setOutput('base-branch', base_branch);
+            core.setOutput('prs-string', prs.join('\n'));
+            
+            combined = branches.join(' ')
+            console.log('Combined: ' + combined);
+            return combined
+
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      # Creates a branch with other PR branches merged together
+      - name: Created combined branch
+        env:
+          BASE_BRANCH: ${{ steps.fetch-branch-names.outputs.base-branch }}
+          BRANCHES_TO_COMBINE: ${{ steps.fetch-branch-names.outputs.result }}
+          COMBINE_BRANCH_NAME: ${{ github.event.inputs.combineBranchName }}
+        run: |
+          echo "$BRANCHES_TO_COMBINE"
+          sourcebranches="${BRANCHES_TO_COMBINE%\"}"
+          sourcebranches="${sourcebranches#\"}"
+          
+          basebranch="${BASE_BRANCH%\"}"
+          basebranch="${basebranch#\"}"
+          
+          git config pull.rebase false
+          git config user.name github-actions
+          git config user.email github-actions@github.com
+          
+          git branch $COMBINE_BRANCH_NAME $basebranch
+          git checkout $COMBINE_BRANCH_NAME
+          git pull origin $sourcebranches --no-edit
+          git push origin $COMBINE_BRANCH_NAME
+
+      # Creates a PR with the new combined branch
+      - uses: actions/github-script@v6
+        name: Create Combined Pull Request
+        env:
+          PRS_STRING: ${{ steps.fetch-branch-names.outputs.prs-string }}
+        with:
+          github-token: ${{secrets.GITHUB_TOKEN}}
+          script: |
+            const prString = process.env.PRS_STRING;
+            const body = 'This PR was created by the Combine PRs action by combining the following PRs:\n' + prString;
+            await github.pulls.create({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              title: 'Combined Dependabot Bumps',
+              head: '${{ github.event.inputs.combineBranchName }}',
+              base: '${{ steps.fetch-branch-names.outputs.base-branch }}',
+              body: body
+            });
diff --git a/.github/workflows/upload-previews.yml b/.github/workflows/upload-previews.yml
index 5325c4939..0bec37843 100644
--- a/.github/workflows/upload-previews.yml
+++ b/.github/workflows/upload-previews.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/setup-python@v1
+      - uses: actions/setup-python@v3
         with:
           python-version: '3.8'
 
diff --git a/.gitignore b/.gitignore
index e0c46182d..55c223952 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,7 @@
 .idea/
 env/
 venv/
-env36/
-env37/
-env38/
-env39/
+env3*/
 Pipfile
 *.lock
 *.py[cod]
diff --git a/changes/3625-read_text-encoding-utf-8.md b/changes/3625-read_text-encoding-utf-8.md
new file mode 100644
index 000000000..a9365e520
--- /dev/null
+++ b/changes/3625-read_text-encoding-utf-8.md
@@ -0,0 +1,1 @@
+Add `read_text(encoding='utf-8')` for `setup.py`
diff --git a/changes/3636-tommilligan.md b/changes/3636-tommilligan.md
new file mode 100644
index 000000000..ec10fce7e
--- /dev/null
+++ b/changes/3636-tommilligan.md
@@ -0,0 +1,1 @@
+fix: clarify that discriminated unions do not support singletons
diff --git a/changes/3706-samuelcolvin.md b/changes/3706-samuelcolvin.md
new file mode 100644
index 000000000..3a22afee6
--- /dev/null
+++ b/changes/3706-samuelcolvin.md
@@ -0,0 +1,1 @@
+Prevent subclasses of bytes being converted to bytes
diff --git a/docs/index.md b/docs/index.md
index 772a6ea99..f06b55583 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -114,6 +114,11 @@ Hundreds of organisations and packages are using *pydantic*, including:
 : trusts *pydantic* (via FastAPI) and [*arq*](https://github.com/samuelcolvin/arq) (Samuel's excellent
   asynchronous task queue) to reliably power multiple mission-critical microservices.
 
+[Robusta.dev](https://robusta.dev/)
+: are using *pydantic* to automate Kubernetes troubleshooting and maintenance. For example, their open source
+  [tools to debug and profile Python applications on Kubernetes](https://home.robusta.dev/python/) use
+  *pydantic* models.
+
 For a more comprehensive list of open-source projects using *pydantic* see the 
 [list of dependents on github](https://github.com/samuelcolvin/pydantic/network/dependents).
 
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 88f06ea3f..8c28433d5 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,12 +1,12 @@
 ansi2html==1.6.0
 flake8==4.0.1
 flake8-quotes==3.3.1
-hypothesis==6.31.6
+hypothesis==6.41.0
 markdown-include==0.6.0
 mdx-truly-sane-lists==1.2
-mkdocs==1.2.3
+mkdocs==1.3.0
 mkdocs-exclude==1.0.2
-mkdocs-material==8.1.3
+mkdocs-material==8.2.8
 sqlalchemy
 orjson
 ujson
diff --git a/docs/usage/settings.md b/docs/usage/settings.md
index 1de49c0a5..3ab582139 100644
--- a/docs/usage/settings.md
+++ b/docs/usage/settings.md
@@ -71,7 +71,7 @@ by treating the environment variable's value as a JSON-encoded string.
 
 Another way to populate nested complex variables is to configure your model with the `env_nested_delimiter`
 config setting, then use an env variable with a name pointing to the nested module fields.
-What it does is simply explodes yor variable into nested models or dicts.
+What it does is simply explodes your variable into nested models or dicts.
 So if you define a variable `FOO__BAR__BAZ=123` it will convert it into `FOO={'BAR': {'BAZ': 123}}`
 If you have multiple variables with the same structure they will be merged.
 
diff --git a/docs/usage/types.md b/docs/usage/types.md
index 3fcd61d31..86a437ba6 100644
--- a/docs/usage/types.md
+++ b/docs/usage/types.md
@@ -297,6 +297,12 @@ _(This script is complete, it should run "as is")_
     Using the [Annotated Fields syntax](../schema/#typingannotated-fields) can be handy to regroup
     the `Union` and `discriminator` information. See below for an example!
 
+!!! warning
+    Discriminated unions cannot be used with only a single variant, such as `Union[Cat]`.
+
+    Python changes `Union[T]` into `T` at interpretation time, so it is not possible for `pydantic` to
+    distinguish fields of `Union[T]` from `T`.
+
 #### Nested Discriminated Unions
 
 Only one discriminator can be set for a field but sometimes you want to combine multiple discriminators.
diff --git a/pydantic/fields.py b/pydantic/fields.py
index 6f1699032..e6736f023 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -600,7 +600,7 @@ class ModelField(Representation):
             return
 
         if self.discriminator_key is not None and not is_union(origin):
-            raise TypeError('`discriminator` can only be used with `Union` type')
+            raise TypeError('`discriminator` can only be used with `Union` type with more than one variant')
 
         # add extra check for `collections.abc.Hashable` for python 3.10+ where origin is not `None`
         if origin is None or origin is CollectionsHashable:
diff --git a/pydantic/json.py b/pydantic/json.py
index b732cc0a2..0769228e4 100644
--- a/pydantic/json.py
+++ b/pydantic/json.py
@@ -26,7 +26,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
 
     This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
     where a integer (but not int typed) is used. Encoding this as a float
-    results in failed round-tripping between encode and prase.
+    results in failed round-tripping between encode and parse.
     Our Id type is a prime example of this.
 
     >>> decimal_encoder(Decimal("1.0"))
diff --git a/pydantic/networks.py b/pydantic/networks.py
index 18a042ca9..50a01d42b 100644
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -122,7 +122,7 @@ def int_domain_regex() -> Pattern[str]:
 class AnyUrl(str):
     strip_whitespace = True
     min_length = 1
-    max_length = 2 ** 16
+    max_length = 2**16
     allowed_schemes: Optional[Collection[str]] = None
     tld_required: bool = False
     user_required: bool = False
@@ -386,7 +386,7 @@ def stricturl(
     *,
     strip_whitespace: bool = True,
     min_length: int = 1,
-    max_length: int = 2 ** 16,
+    max_length: int = 2**16,
     tld_required: bool = True,
     host_required: bool = True,
     allowed_schemes: Optional[Collection[str]] = None,
diff --git a/pydantic/types.py b/pydantic/types.py
index e3d6c1277..2d0cc18f8 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -1010,18 +1010,18 @@ class PaymentCardNumber(str):
 
 BYTE_SIZES = {
     'b': 1,
-    'kb': 10 ** 3,
-    'mb': 10 ** 6,
-    'gb': 10 ** 9,
-    'tb': 10 ** 12,
-    'pb': 10 ** 15,
-    'eb': 10 ** 18,
-    'kib': 2 ** 10,
-    'mib': 2 ** 20,
-    'gib': 2 ** 30,
-    'tib': 2 ** 40,
-    'pib': 2 ** 50,
-    'eib': 2 ** 60,
+    'kb': 10**3,
+    'mb': 10**6,
+    'gb': 10**9,
+    'tb': 10**12,
+    'pb': 10**15,
+    'eb': 10**18,
+    'kib': 2**10,
+    'mib': 2**20,
+    'gib': 2**30,
+    'tib': 2**40,
+    'pib': 2**50,
+    'eib': 2**60,
 }
 BYTE_SIZES.update({k.lower()[0]: v for k, v in BYTE_SIZES.items() if 'i' not in k})
 byte_string_re = re.compile(r'^\s*(\d*\.?\d+)\s*(\w+)?', re.IGNORECASE)
diff --git a/pydantic/validators.py b/pydantic/validators.py
index 63b7a59e0..d4783d97b 100644
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -76,7 +76,7 @@ def strict_str_validator(v: Any) -> Union[str]:
     raise errors.StrError()
 
 
-def bytes_validator(v: Any) -> bytes:
+def bytes_validator(v: Any) -> Union[bytes]:
     if isinstance(v, bytes):
         return v
     elif isinstance(v, bytearray):
diff --git a/requirements.txt b/requirements.txt
index 95aef0852..3beeb0fdb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
 # requirements for compilation and from setup.py so dependabot prompts us to test with latest version of these packages
 
-Cython==0.29.26;sys_platform!='win32'
+Cython==0.29.28;sys_platform!='win32'
 devtools==0.8.0
 email-validator==1.1.3
 dataclasses==0.6; python_version < '3.7'
-typing-extensions==4.0.1
-python-dotenv==0.19.2
+typing-extensions==4.1.1
+python-dotenv==0.20.0
diff --git a/setup.py b/setup.py
index ae91cf9a0..c59056e0e 100644
--- a/setup.py
+++ b/setup.py
@@ -59,12 +59,12 @@ class ReplaceLinks:
 description = 'Data validation and settings management using python type hints'
 THIS_DIR = Path(__file__).resolve().parent
 try:
-    history = (THIS_DIR / 'HISTORY.md').read_text()
+    history = (THIS_DIR / 'HISTORY.md').read_text(encoding='utf-8')
     history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/pydantic/issues/\1)', history)
     history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I)
     history = re.sub('@@', '@', history)
 
-    long_description = (THIS_DIR / 'README.md').read_text() + '\n\n' + history
+    long_description = (THIS_DIR / 'README.md').read_text(encoding='utf-8') + '\n\n' + history
 except FileNotFoundError:
     long_description = description + '.\n\nSee https://pydantic-docs.helpmanual.io/ for documentation.'
 


Test patch:
diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt
index 9a3f0464b..69487560e 100644
--- a/tests/requirements-linting.txt
+++ b/tests/requirements-linting.txt
@@ -1,10 +1,10 @@
-black==21.12b0
+black==22.3.0
 flake8==4.0.1
 flake8-quotes==3.3.1
-hypothesis==6.31.6
+hypothesis==6.41.0
 isort==5.10.1
-mypy==0.930
-pre-commit==2.16.0
+mypy==0.931
+pre-commit==2.17.0
 pycodestyle==2.8.0
 pyflakes==2.4.0
-twine==3.7.1
+twine==4.0.0
diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt
index 1ee135251..e4f808bf8 100644
--- a/tests/requirements-testing.txt
+++ b/tests/requirements-testing.txt
@@ -1,9 +1,9 @@
-coverage==6.2
-hypothesis==6.31.6
+coverage==6.3.2
+hypothesis==6.41.0
 # pin importlib-metadata as upper versions need typing-extensions to work if on python < 3.8
 importlib-metadata==3.1.0;python_version<"3.8"
-mypy==0.930
-pytest==6.2.5
+mypy==0.931
+pytest==7.1.1
 pytest-cov==3.0.0
-pytest-mock==3.6.1
+pytest-mock==3.7.0
 pytest-sugar==0.9.4
diff --git a/tests/test_construction.py b/tests/test_construction.py
index 19e912b39..e7b12a0a7 100644
--- a/tests/test_construction.py
+++ b/tests/test_construction.py
@@ -63,8 +63,8 @@ def test_large_any_str():
         a: bytes
         b: str
 
-    content_bytes = b'x' * (2 ** 16 + 1)
-    content_str = 'x' * (2 ** 16 + 1)
+    content_bytes = b'x' * (2**16 + 1)
+    content_str = 'x' * (2**16 + 1)
     m = Model(a=content_bytes, b=content_str)
     assert m.a == content_bytes
     assert m.b == content_str
diff --git a/tests/test_discrimated_union.py b/tests/test_discrimated_union.py
index 120f9d2f1..a4dd501bc 100644
--- a/tests/test_discrimated_union.py
+++ b/tests/test_discrimated_union.py
@@ -11,12 +11,23 @@ from pydantic.generics import GenericModel
 
 
 def test_discriminated_union_only_union():
-    with pytest.raises(TypeError, match='`discriminator` can only be used with `Union` type'):
+    with pytest.raises(
+        TypeError, match='`discriminator` can only be used with `Union` type with more than one variant'
+    ):
 
         class Model(BaseModel):
             x: str = Field(..., discriminator='qwe')
 
 
+def test_discriminated_union_single_variant():
+    with pytest.raises(
+        TypeError, match='`discriminator` can only be used with `Union` type with more than one variant'
+    ):
+
+        class Model(BaseModel):
+            x: Union[str] = Field(..., discriminator='qwe')
+
+
 def test_discriminated_union_invalid_type():
     with pytest.raises(TypeError, match="Type 'str' is not a valid `BaseModel` or `dataclass`"):
 
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index dd07eb3d3..5da622570 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -1906,3 +1906,29 @@ def test_arbitrary_types_allowed_custom_eq():
             arbitrary_types_allowed = True
 
     assert Model().x == Foo()
+
+
+def test_bytes_subclass():
+    class MyModel(BaseModel):
+        my_bytes: bytes
+
+    class BytesSubclass(bytes):
+        def __new__(cls, data: bytes):
+            self = bytes.__new__(cls, data)
+            return self
+
+    m = MyModel(my_bytes=BytesSubclass(b'foobar'))
+    assert m.my_bytes.__class__ == BytesSubclass
+
+
+def test_int_subclass():
+    class MyModel(BaseModel):
+        my_int: int
+
+    class IntSubclass(int):
+        def __new__(cls, data: int):
+            self = int.__new__(cls, data)
+            return self
+
+    m = MyModel(my_int=IntSubclass(123))
+    assert m.my_int.__class__ == IntSubclass
diff --git a/tests/test_main.py b/tests/test_main.py
index 642b5610b..c00d70688 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -433,13 +433,13 @@ def test_with_declared_hash():
         x: int
 
         def __hash__(self):
-            return self.x ** 2
+            return self.x**2
 
     class Bar(Foo):
         y: int
 
         def __hash__(self):
-            return self.y ** 3
+            return self.y**3
 
     class Buz(Bar):
         z: int
diff --git a/tests/test_networks_ipaddress.py b/tests/test_networks_ipaddress.py
index cd1351b59..c73c51230 100644
--- a/tests/test_networks_ipaddress.py
+++ b/tests/test_networks_ipaddress.py
@@ -114,7 +114,7 @@ def test_ipv6address_success(value):
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}],
         ),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}],
         ),
     ],
@@ -141,7 +141,7 @@ def test_ipaddress_fails(value, errors):
         ),
         (-1, [{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}]),
         (
-            2 ** 32 + 1,
+            2**32 + 1,
             [{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}],
         ),
         (
@@ -172,7 +172,7 @@ def test_ipv4address_fails(value, errors):
         ),
         (-1, [{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}]),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}],
         ),
         (
@@ -203,7 +203,7 @@ def test_ipv6address_fails(value, errors):
         ('192.168.0.0/24', IPv4Network),
         ('192.168.128.0/30', IPv4Network),
         ('2001:db00::0/120', IPv6Network),
-        (2 ** 32 - 1, IPv4Network),  # no mask equals to mask /32
+        (2**32 - 1, IPv4Network),  # no mask equals to mask /32
         (20_282_409_603_651_670_423_947_251_286_015, IPv6Network),  # /128
         (b'\xff\xff\xff\xff', IPv4Network),  # /32
         (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Network),
@@ -224,7 +224,7 @@ def test_ipnetwork_success(value, cls):
     [
         ('192.168.0.0/24', IPv4Network),
         ('192.168.128.0/30', IPv4Network),
-        (2 ** 32 - 1, IPv4Network),  # no mask equals to mask /32
+        (2**32 - 1, IPv4Network),  # no mask equals to mask /32
         (b'\xff\xff\xff\xff', IPv4Network),  # /32
         (('192.168.0.0', 24), IPv4Network),
         (IPv4Network('192.168.0.0/24'), IPv4Network),
@@ -270,7 +270,7 @@ def test_ip_v6_network_success(value, cls):
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}],
         ),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}],
         ),
     ],
@@ -297,7 +297,7 @@ def test_ipnetwork_fails(value, errors):
         ),
         (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}]),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}],
         ),
         (
@@ -328,7 +328,7 @@ def test_ip_v4_network_fails(value, errors):
         ),
         (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}]),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}],
         ),
         (
@@ -362,8 +362,8 @@ def test_ip_v6_network_fails(value, errors):
         ('192.168.128.1/30', IPv4Interface),
         ('2001:db00::0/120', IPv6Interface),
         ('2001:db00::1/120', IPv6Interface),
-        (2 ** 32 - 1, IPv4Interface),  # no mask equals to mask /32
-        (2 ** 32 - 1, IPv4Interface),  # so ``strict`` has no effect
+        (2**32 - 1, IPv4Interface),  # no mask equals to mask /32
+        (2**32 - 1, IPv4Interface),  # so ``strict`` has no effect
         (20_282_409_603_651_670_423_947_251_286_015, IPv6Interface),  # /128
         (20_282_409_603_651_670_423_947_251_286_014, IPv6Interface),
         (b'\xff\xff\xff\xff', IPv4Interface),  # /32
@@ -394,8 +394,8 @@ def test_ipinterface_success(value, cls):
         ('192.168.0.1/24', IPv4Interface),
         ('192.168.128.0/30', IPv4Interface),
         ('192.168.128.1/30', IPv4Interface),
-        (2 ** 32 - 1, IPv4Interface),  # no mask equals to mask /32
-        (2 ** 32 - 1, IPv4Interface),  # so ``strict`` has no effect
+        (2**32 - 1, IPv4Interface),  # no mask equals to mask /32
+        (2**32 - 1, IPv4Interface),  # so ``strict`` has no effect
         (b'\xff\xff\xff\xff', IPv4Interface),  # /32
         (b'\xff\xff\xff\xff', IPv4Interface),
         (('192.168.0.0', 24), IPv4Interface),
@@ -467,7 +467,7 @@ def test_ip_v6_interface_success(value, cls):
             ],
         ),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [
                 {
                     'loc': ('ip',),
@@ -500,7 +500,7 @@ def test_ipinterface_fails(value, errors):
         ),
         (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}]),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}],
         ),
     ],
@@ -527,7 +527,7 @@ def test_ip_v4_interface_fails(value, errors):
         ),
         (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}]),
         (
-            2 ** 128 + 1,
+            2**128 + 1,
             [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}],
         ),
     ],
diff --git a/tests/test_schema.py b/tests/test_schema.py
index ea0290f09..838883109 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -799,7 +799,7 @@ def test_str_constrained_types(field_type, expected_schema):
 @pytest.mark.parametrize(
     'field_type,expected_schema',
     [
-        (AnyUrl, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}),
+        (AnyUrl, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2**16}),
         (
             stricturl(min_length=5, max_length=10),
             {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10},
diff --git a/tests/test_types.py b/tests/test_types.py
index a7b7db89f..8e5481719 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1144,7 +1144,7 @@ def test_dict():
         ([1, 2, '3'], [1, 2, '3']),
         ((1, 2, '3'), [1, 2, '3']),
         ({1, 2, '3'}, list({1, 2, '3'})),
-        ((i ** 2 for i in range(5)), [0, 1, 4, 9, 16]),
+        ((i**2 for i in range(5)), [0, 1, 4, 9, 16]),
         ((deque((1, 2, 3)), list(deque((1, 2, 3))))),
     ),
 )
@@ -1184,7 +1184,7 @@ def test_ordered_dict():
         ([1, 2, '3'], (1, 2, '3')),
         ((1, 2, '3'), (1, 2, '3')),
         ({1, 2, '3'}, tuple({1, 2, '3'})),
-        ((i ** 2 for i in range(5)), (0, 1, 4, 9, 16)),
+        ((i**2 for i in range(5)), (0, 1, 4, 9, 16)),
         (deque([1, 2, 3]), (1, 2, 3)),
     ),
 )
@@ -1210,7 +1210,7 @@ def test_tuple_fails(value):
     (
         ([1, 2, '3'], int, (1, 2, 3)),
         ((1, 2, '3'), int, (1, 2, 3)),
-        ((i ** 2 for i in range(5)), int, (0, 1, 4, 9, 16)),
+        ((i**2 for i in range(5)), int, (0, 1, 4, 9, 16)),
         (('a', 'b', 'c'), str, ('a', 'b', 'c')),
     ),
 )
@@ -1250,7 +1250,7 @@ def test_tuple_variable_len_fails(value, cls, exc):
         ({1, 2, 2, '3'}, {1, 2, '3'}),
         ((1, 2, 2, '3'), {1, 2, '3'}),
         ([1, 2, 2, '3'], {1, 2, '3'}),
-        ({i ** 2 for i in range(5)}, {0, 1, 4, 9, 16}),
+        ({i**2 for i in range(5)}, {0, 1, 4, 9, 16}),
     ),
 )
 def test_set_success(value, result):