Repository: samuelcolvin/pydantic
Pull request #3968 (2022-04-02 07:57:35)

build(deps): bump mypy from 0.931 to 0.942
Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.942.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/python/mypy/commit/38f1e30e8137ccc1aad6a4f113eb4360c6206539"><code>38f1e30</code></a> Update version to 0.942</li>
<li><a href="https://github.com/python/mypy/commit/1c836685da13f11287ae6d6931c04337f881ec40"><code>1c83668</code></a> Let overload item have a wider return type than implementation (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12435">#12435</a>)</li>
<li><a href="https://github.com/python/mypy/commit/67088e558dc24a2c6c231db542a367923dfdc049"><code>67088e5</code></a> Pin the version of bugbear (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12436">#12436</a>)</li>
<li><a href="https://github.com/python/mypy/commit/367b29d4aac16fc7493abffe2df0d8f477c23923"><code>367b29d</code></a> Make order of processing the builtins SCC predictable (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12431">#12431</a>)</li>
<li><a href="https://github.com/python/mypy/commit/f81b228e66d8a95cc39247f189e7be7e894f7f92"><code>f81b228</code></a> Fix inheritance false positives with dataclasses/attrs (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12411">#12411</a>)</li>
<li><a href="https://github.com/python/mypy/commit/7e09c2a100209072429e290d2f7b9b8007b8629c"><code>7e09c2a</code></a> Support overriding dunder attributes in Enum subclass (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12138">#12138</a>)</li>
<li><a href="https://github.com/python/mypy/commit/837543efb616b14e2f800db6962d216621dee4d7"><code>837543e</code></a> Fix crash in match statement if class name is undefined (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12417">#12417</a>)</li>
<li><a href="https://github.com/python/mypy/commit/6606dbe98d09170d3ad810bc791a16d99ceb2281"><code>6606dbe</code></a> Allow non-final <strong>match_args</strong> and overriding (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12415">#12415</a>)</li>
<li><a href="https://github.com/python/mypy/commit/626147a761891362dbf6e845c99031e2e043d0f4"><code>626147a</code></a> Fix small conditional overload regression (<a href="https://github-redirect.dependabot.com/python/mypy/issues/12336">#12336</a>)</li>
<li><a href="https://github.com/python/mypy/commit/8e9ac1576b2d9b93bfe67dcf3a553e479f9af971"><code>8e9ac15</code></a> Bump version to 0.942+dev</li>
<li>Additional commits viewable in <a href="https://github.com/python/mypy/compare/v0.931...v0.942">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mypy&package-manager=pip&previous-version=0.931&new-version=0.942)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


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/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/.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/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/networks.py b/pydantic/networks.py
index 50a01d42b..cbc9e21f4 100644
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -265,7 +265,7 @@ class AnyUrl(str):
     def validate_host(cls, parts: 'Parts') -> Tuple[str, Optional[str], str, bool]:
         host, tld, host_type, rebuild = None, None, None, False
         for f in ('domain', 'ipv4', 'ipv6'):
-            host = parts[f]  # type: ignore[misc]
+            host = parts[f]  # type: ignore[literal-required]
             if host:
                 host_type = f
                 break
@@ -310,8 +310,8 @@ class AnyUrl(str):
     @classmethod
     def apply_default_parts(cls, parts: 'Parts') -> 'Parts':
         for key, value in cls.get_default_parts(parts).items():
-            if not parts[key]:  # type: ignore[misc]
-                parts[key] = value  # type: ignore[misc]
+            if not parts[key]:  # type: ignore[literal-required]
+                parts[key] = value  # type: ignore[literal-required]
         return parts
 
     def __repr__(self) -> str:
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/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 69487560e..f33bda7f7 100644
--- a/tests/requirements-linting.txt
+++ b/tests/requirements-linting.txt
@@ -3,7 +3,7 @@ flake8==4.0.1
 flake8-quotes==3.3.1
 hypothesis==6.41.0
 isort==5.10.1
-mypy==0.931
+mypy==0.942
 pre-commit==2.17.0
 pycodestyle==2.8.0
 pyflakes==2.4.0
diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt
index e4f808bf8..f29e6454b 100644
--- a/tests/requirements-testing.txt
+++ b/tests/requirements-testing.txt
@@ -2,7 +2,7 @@ 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.931
+mypy==0.942
 pytest==7.1.1
 pytest-cov==3.0.0
 pytest-mock==3.7.0
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