Repository: tomerfiliba/plumbum

Issue:
Future support for from __future__ import annotations
I added `YAMLWizard` to some code using plumbum's `cli`, and after added the required `__future__` line I discovered that my `main` function wouldn't typecheck its command line arguments anymore. Picture something like this:

```
from __future__ import annotations
from plumbum import cli
# import YAMLWizard # skipped for simplicity

class app(cli.Application):
    def main(self, *dirs: cli.ExistingDirectory):
          pass
```

I hope it's clear what's expected here. Anyhow, what will actually happen due to the `annotations` feature is that the annotations get processed differently, and apparently don't work the same way. So instead of typechecking the command line as expected, the program always terminates with a runtime error that "str object is not callable."

Workarounds:

1. Just separate cli.Application and __future__.annotation use into separate modules. It's what I'm doing now.
2. Don't typecheck the command line. I did that for a bit, but it's annoying.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/53043273/badge)](https://coveralls.io/builds/53043273)

Coverage decreased (-0.0008%) to 83.964% when pulling **888a5eb377a439560d00692e1e56d93c2cb98c7d on henryiii/fix/futureann** into **da87b67d3efcaa61554756d56775ac44a3379c00 on master**.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f65c073..e53a554 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,7 +33,7 @@ jobs:
     - name: pylint
       run: |
         echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json"
-        pipx run nox -s pylint
+        pipx run --python python nox -s pylint
 
   tests:
     name: Tests on 🐍 ${{ matrix.python-version }} ${{ matrix.os }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3f5f487..3770b2e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -81,7 +81,6 @@ repos:
   - id: python-check-blanket-noqa
   - id: python-check-blanket-type-ignore
   - id: python-no-log-warn
-  - id: python-no-eval
   - id: python-use-type-annotations
   - id: rst-backticks
   - id: rst-directive-colons
diff --git a/plumbum/cli/application.py b/plumbum/cli/application.py
index 788ccb5..d83e6f1 100644
--- a/plumbum/cli/application.py
+++ b/plumbum/cli/application.py
@@ -501,6 +501,12 @@ class Application:
                 )
 
         m = inspect.getfullargspec(self.main)
+
+        if sys.version_info < (3, 10):
+            sig = inspect.signature(self.main)
+        else:
+            sig = inspect.signature(self.main, eval_str=True)
+
         max_args = sys.maxsize if m.varargs else len(m.args) - 1
         min_args = len(m.args) - 1 - (len(m.defaults) if m.defaults else 0)
         if len(tailargs) < min_args:
@@ -530,17 +536,24 @@ class Application:
                 m.varargs,
             )
 
-        elif hasattr(m, "annotations"):
+        elif hasattr(m, "annotations") and m.annotations:
             args_names = list(m.args[1:])
             positional = [None] * len(args_names)
             varargs = None
 
             # All args are positional, so convert kargs to positional
             for item in m.annotations:
+                annotation = (
+                    sig.parameters[item].annotation
+                    if item != "return"
+                    else sig.return_annotation
+                )
+                if sys.version_info < (3, 10) and isinstance(annotation, str):
+                    annotation = eval(annotation)
                 if item == m.varargs:
-                    varargs = m.annotations[item]
+                    varargs = annotation
                 elif item != "return":
-                    positional[args_names.index(item)] = m.annotations[item]
+                    positional[args_names.index(item)] = annotation
 
             tailargs = self._positional_validate(
                 tailargs, positional, varargs, m.args[1:], m.varargs
diff --git a/pyproject.toml b/pyproject.toml
index bf9042f..8a7ee77 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -106,4 +106,5 @@ messages_control.disable = [
   "unidiomatic-typecheck", # TODO: might be able to remove
   "unnecessary-lambda-assignment", # TODO: 4 instances
   "unused-import", # identical to flake8 but has typing false positives
+  "eval-used",  # Needed for Python <3.10 annotations
 ]


Test patch:
diff --git a/tests/test_3_cli.py b/tests/test_3_cli.py
index 9b8af84..279a56b 100644
--- a/tests/test_3_cli.py
+++ b/tests/test_3_cli.py
@@ -14,7 +14,7 @@ class TestProg3:
 
 
 class Main4Validator(cli.Application):
-    def main(self, myint: int, myint2: int, *mylist: int) -> None:
+    def main(self, myint: int, myint2: int, *mylist: "int") -> None:
         print(myint, myint2, mylist)