Repository: python/mypy

Issue:
How to use an implicit-optional library in a no_implicit_optional codebase
Given a library which uses `no_implicit_optional = False` (the default) and thus defines functions like:
``` python
def foo(bar: str = None): ...
```
(where `bar` is implicitly `Optional[str]`)

I'm trying to work out how to call such a function in a project where I want to have set `no_implicit_optional = True`.

``` python
def myfunc(bar: Optional[str] = None):
    # Argument 1 to "foo" has incompatible type "Optional[str]"; expected "str"
    that_library.foo(bar)
```

My current workaround is to `cast` the value of `bar` to the non-optional type before passing it on, however this doesn't feel like a good solution.

I've also tried configuring `mypy` with a different setting for the library, via the following in my `setup.cfg`:
``` ini
[mypy]
no_implicit_optional = True

[mypy-that_library]
no_implicit_optional = False
```

What is the recommended approach for this?

It's worth noting that the signature reported by `reveal_type` also changes in response to the local value of this setting.
Is it intentional that such library APIs change their signature based on the local value of `no_implicit_optional`?

---
I'm using mypy 0.782 on Python 3.6.

In case it's useful, the actual place I'm hitting this is in using [`httpx`](https://pypi.org/project/httpx): https://github.com/PeterJCLaw/code-submitter/blob/79d5cc7539d47f7effd27e7d8c630badf75d9e0d/code_submitter/auth.py#L94-L106



Discussion:
> What is the recommended approach for this?

Setting this in the configuration file for the library is the recommended approach.

> Is it intentional that such library APIs change their signature based on the local value of no_implicit_optional?

I guess so, though it's questionable if this is desirable.

We are probably going to turn `no_implicit_optional` to true by default in a future mypy releases and deprecate the current default eventually.
> > What is the recommended approach for this?
> 
> Setting this in the configuration file for the library is the recommended approach.

Thanks :)

As that doesn't seem to work (I've also hit it not working with other settings, notably `implicit_reexport`), should I raise a different issue, or are you happy to use this to track it?

> As that doesn't seem to work (I've also hit it not working with other settings, notably `implicit_reexport`), should I raise a different issue, or are you happy to use this to track it?

Let's continue here. Show us exactly what you did (what you put in your config file *and* on the mypy command line), which mypy version, and some of the code and errors that makes you believe it doesn't work.
Sure. Here's the cut-down I'm using to explore this:
``` shell
# Setup venv
python3 -m venv venv
.  venv/bin/activate
pip install mypy

# Create that_library
mkdir venv/lib/python3.8/site-packages/that_library/
touch venv/lib/python3.8/site-packages/that_library/py.typed
cat - > venv/lib/python3.8/site-packages/that_library/__init__.py <<EOF
def foo(bar: str = None): ...
EOF

# Setup local project
cat - > setup.cfg <<EOF
[mypy]
no_implicit_optional = True

[mypy-that_library]
no_implicit_optional = False
EOF

cat - > mine.py <<EOF
from typing import Optional
import that_library

def myfunc(bar: Optional[str] = None):
    that_library.foo(bar)
EOF

# Run mypy
mypy mine.py
```

With outputs:
```
$ mypy mine.py
mine.py:5: error: Argument 1 to "foo" has incompatible type "Optional[str]"; expected "str"
Found 1 error in 1 file (checked 1 source file)
```

This is the error I'm trying to solve by adding the `mypy-that_library` config.

I'm not sure if I was expecting the config I added would help solve this, however that was the cause of my original question.

I'm guessing that `mypy` is evaluating the config for `mine.py` at this point, rather than considering that the `that_library.foo` call is part of `that_library` and should therefore have implicit-optional behaviour?

---
I'm using mypy 0.782 and have tried both Python 3.6 and 3.8.
Awesome. I reproduced this and I think I have diagnosed it, but I don't have a fix yet. It seems that when mypy parses the file it passes in the wrong Options object. It correctly sets State.options to the module-cloned Options object (where the flag is False) but it passes BuildManager.options to the parse() function, and that is still the global set of options, where the flag is True.

I will come up with a PR to fix this in the coming days.
Amazing, thanks!

Resolving patch:
diff --git a/mypy/build.py b/mypy/build.py
--- a/mypy/build.py
+++ b/mypy/build.py
@@ -762,13 +762,14 @@ def is_module(self, id: str) -> bool:
         """Is there a file in the file system corresponding to module id?"""
         return find_module_simple(id, self) is not None
 
-    def parse_file(self, id: str, path: str, source: str, ignore_errors: bool) -> MypyFile:
+    def parse_file(self, id: str, path: str, source: str, ignore_errors: bool,
+                   options: Options) -> MypyFile:
         """Parse the source of a file with the given name.
 
         Raise CompileError if there is a parse error.
         """
         t0 = time.time()
-        tree = parse(source, path, id, self.errors, options=self.options)
+        tree = parse(source, path, id, self.errors, options=options)
         tree._fullname = id
         self.add_stats(files_parsed=1,
                        modules_parsed=int(not tree.is_stub),
@@ -2001,7 +2002,8 @@ def parse_file(self) -> None:
 
             self.parse_inline_configuration(source)
             self.tree = manager.parse_file(self.id, self.xpath, source,
-                                           self.ignore_all or self.options.ignore_errors)
+                                           self.ignore_all or self.options.ignore_errors,
+                                           self.options)
 
         modules[self.id] = self.tree
 


Test patch:
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -1501,3 +1501,32 @@ class ShouldNotBeFine(x): ...  # E: Class cannot subclass 'x' (has type 'Any')
 disallow_subclassing_any = True
 \[mypy-m]
 disallow_subclassing_any = False
+
+[case testNoImplicitOptionalPerModule]
+# flags: --config-file tmp/mypy.ini
+import m
+
+[file m.py]
+def f(a: str = None) -> int:
+    return 0
+
+[file mypy.ini]
+\[mypy]
+no_implicit_optional = True
+\[mypy-m]
+no_implicit_optional = False
+
+[case testNoImplicitOptionalPerModulePython2]
+# flags: --config-file tmp/mypy.ini --python-version 2.7
+import m
+
+[file m.py]
+def f(a = None):
+    # type: (str) -> int
+    return 0
+
+[file mypy.ini]
+\[mypy]
+no_implicit_optional = True
+\[mypy-m]
+no_implicit_optional = False