-
Describe the bugTyper doesn't support freezegun's To Reproduce
from datetime import datetime
import typer
app = typer.Typer()
@app.command()
def data(
year_month: datetime = typer.Argument(
f"{datetime.today():%Y-%m}", formats=["%Y-%m"]
)
):
typer.echo(f"Data for {year_month:%Y-%b}")
if __name__ == "__main__":
app()
from typer.testing import CliRunner
from main import app
runner = CliRunner()
def test_data(freezer):
freezer.move_to("2020-06-01")
result = runner.invoke(app)
assert "Data for 2020-June" in result.stdout
pytest -k test_data
Complete Stacktrace-> % pytest -k test_data
=================================================================================================== test session starts ===================================================================================================
platform darwin -- Python 3.8.7, pytest-5.4.3, py-1.8.1, pluggy-0.13.1
rootdir: ~/my_project, inifile: pytest.ini, testpaths: tests
plugins: pylama-7.7.1, cov-2.10.0, freezegun-0.4.1, mock-3.1.0, recording-0.8.1, socket-0.4.0, spec-3.2.0, testmon-1.1.0
collected 31 items / 30 deselected / 1 selected
tests/cli/data/test_defaults.py:
✗ Data [100%]
======================================================================================================== FAILURES =========================================================================================================
________________________________________________________________________________________________________ test_data ________________________________________________________________________________________________________
freezer = <freezegun.api.FrozenDateTimeFactory object at 0x10be4ba30>
def test_data(freezer):
freezer.move_to("2020-06-01")
> result = runner.invoke(app)
tests/cli/data/test_defaults.py:10:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
~/Library/Caches/pypoetry/virtualenvs/my_project-BAzU1U47-py3.8/lib/python3.8/site-packages/typer/testing.py:20: in invoke
use_cli = _get_command(app)
~/Library/Caches/pypoetry/virtualenvs/my_project-BAzU1U47-py3.8/lib/python3.8/site-packages/typer/main.py:239: in get_command
click_command = get_command_from_info(typer_instance.registered_commands[0])
~/Library/Caches/pypoetry/virtualenvs/my_project-BAzU1U47-py3.8/lib/python3.8/site-packages/typer/main.py:423: in get_command_from_info
) = get_params_convertors_ctx_param_name_from_function(command_info.callback)
~/Library/Caches/pypoetry/virtualenvs/my_project-BAzU1U47-py3.8/lib/python3.8/site-packages/typer/main.py:404: in get_params_convertors_ctx_param_name_from_function
click_param, convertor = get_click_param(param)
~/Library/Caches/pypoetry/virtualenvs/my_project-BAzU1U47-py3.8/lib/python3.8/site-packages/typer/main.py:656: in get_click_param
parameter_type = get_click_type(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def get_click_type(
*, annotation: Any, parameter_info: ParameterInfo
) -> click.ParamType:
if annotation == str:
return click.STRING
elif annotation == int:
if parameter_info.min is not None or parameter_info.max is not None:
min_ = None
max_ = None
if parameter_info.min is not None:
min_ = int(parameter_info.min)
if parameter_info.max is not None:
max_ = int(parameter_info.max)
return click.IntRange(min=min_, max=max_, clamp=parameter_info.clamp)
else:
return click.INT
elif annotation == float:
if parameter_info.min is not None or parameter_info.max is not None:
return click.FloatRange(
min=parameter_info.min,
max=parameter_info.max,
clamp=parameter_info.clamp,
)
else:
return click.FLOAT
elif annotation == bool:
return click.BOOL
elif annotation == UUID:
return click.UUID
elif annotation == datetime:
return click.DateTime(formats=parameter_info.formats)
elif (
annotation == Path
or parameter_info.allow_dash
or parameter_info.path_type
or parameter_info.resolve_path
):
return click.Path( # type: ignore
exists=parameter_info.exists,
file_okay=parameter_info.file_okay,
dir_okay=parameter_info.dir_okay,
writable=parameter_info.writable,
readable=parameter_info.readable,
resolve_path=parameter_info.resolve_path,
allow_dash=parameter_info.allow_dash,
path_type=parameter_info.path_type,
)
elif lenient_issubclass(annotation, FileTextWrite):
return click.File(
mode=parameter_info.mode or "w",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
lazy=parameter_info.lazy,
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, FileText):
return click.File(
mode=parameter_info.mode or "r",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
lazy=parameter_info.lazy,
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, FileBinaryRead):
return click.File(
mode=parameter_info.mode or "rb",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
lazy=parameter_info.lazy,
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, FileBinaryWrite):
return click.File(
mode=parameter_info.mode or "wb",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
lazy=parameter_info.lazy,
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, Enum):
return click.Choice(
[item.value for item in annotation],
case_sensitive=parameter_info.case_sensitive,
)
> raise RuntimeError(f"Type not yet supported: {annotation}") # pragma no cover
E RuntimeError: Type not yet supported: <class 'datetime.datetime'>
~/Library/Caches/pypoetry/virtualenvs/my_project-BAzU1U47-py3.8/lib/python3.8/site-packages/typer/main.py:587: RuntimeError
================================================================================================= short test summary info =================================================================================================
FAILED tests/cli/data/test_defaults.py::test_data - RuntimeError: Type not yet supported: <class 'datetime.datetime'>
============================================================================================ 1 failed, 30 deselected in 0.44s =============================================================================================
Expected behaviourFreezing times in tests are common. I expect that the test pass. Environment
|
Beta Was this translation helpful? Give feedback.
Replies: 7 comments
-
Hi, I did not use pytest-freezer beforehand and I have no knowledge of how it works internally. def test_data(
# freezer,
):
# freezer.move_to("2020-06-01")
result = runner.invoke(app,)
print(result.stdout)
assert "Data for 2020-Jun" in result.stdout Raises
def test_data(
# freezer,
):
# freezer.move_to("2020-06-01")
result = runner.invoke(app, ["2020-06"])
print(result.stdout)
assert "Data for 2020-Jun" in result.stdout passes
def test_data(
freezer,
):
# freezer.move_to("2020-06-01")
result = runner.invoke(app, ["2020-06"])
print(result.stdout)
assert "Data for 2020-Jun" in result.stdout Raises the One thing to point here is that def main(something: int): You would have to invoke this with string anyway runner.invoke(main, "4") When running runner.invoke(main, 4) I am getting: E + where '' = <Result TypeError("'int' object is not iterable")>.stdout So the module expects a string anyway. Have you maybe checked out if this works with |
Beta Was this translation helpful? Give feedback.
-
Sorry for the clunky sample code there. It should be what I intended now. The main change of the sample code is that typer.Argument now has a default value. Doing The only idea I have is that since freezer/freezegun "changes" |
Beta Was this translation helpful? Give feedback.
-
I did some print debugging within typer code and the line def get_click_type(
*, annotation: Any, parameter_info: ParameterInfo
) -> click.ParamType: In the body, it performs a series of I added two magic lines: def get_click_type(
*, annotation: Any, parameter_info: ParameterInfo
) -> click.ParamType:
print("annonotation: ", annotation)
print("the datetime: ", datetime)
and this gives me such a result: annonotation: <class 'datetime.datetime'>
the datetime: <class 'freezegun.api.FakeDatetime'> Now one of the |
Beta Was this translation helpful? Give feedback.
-
I did one more check to confirm the observation: Only main dependencies are I have two files:
import datetime
def datetime_annotated(
some_date: datetime.datetime
) -> None:
print(some_date) and from main import datetime_annotated
import datetime
def test_datetime_annotation_is_datetime(
freezer
):
annotations = datetime_annotated.__annotations__
some_date_annotation = annotations["some_date"]
assert datetime.datetime == some_date_annotation Result of the run is failure: test_datetime.py:9 (test_datetime_annotation_is_datetime)
<class 'freezegun.api.FakeDatetime'> != <class 'datetime.datetime'>
Expected :<class 'datetime.datetime'>
Actual :<class 'freezegun.api.FakeDatetime'> It means that freezegun substitutes the Below also test without from freezegun import freeze_time
from main import datetime_annotated
import datetime
@freeze_time("2020-06-24")
def test_datetime_annotation_is_datetime(
):
annotations = datetime_annotated.__annotations__
some_date_annotation = annotations["some_date"]
assert datetime.datetime == some_date_annotation Result is the same as with |
Beta Was this translation helpful? Give feedback.
-
Before I open an issue over at
In freezegun/api.py#L345 |
Beta Was this translation helpful? Give feedback.
-
I am no maintainer but in my opinion handling a case, where some library substitutes some object at runtime and forgets about annotations does not justify a change in the library that does not depend on it. I have a possible solution for you, however! With three files:
from datetime import datetime
import typer
app = typer.Typer()
@app.command()
def data(
year_month: datetime = typer.Argument(
f"{datetime.now().strftime('%Y-%m')}",
formats=["%Y-%m"])
):
print("Year-month: ", year_month)
typer.echo(f"Data for {year_month:%Y-%b}")
def datetime_annotated(
some_date: datetime
) -> None:
print(some_date)
if __name__ == "__main__":
app()
import datetime
from freezegun import freeze_time
from typer.testing import CliRunner
from main import app, datetime_annotated
from monkey_patch_typer import monkey_patch_commands, patch_annotations
runner = CliRunner()
def test_data(
freezer,
):
freezer.move_to("2020-06-01")
result = runner.invoke(monkey_patch_commands(app), )
assert "Data for 2021-May" in result.stdout
def test_data_with_argument(
freezer,
):
freezer.move_to("2020-06-01")
result = runner.invoke(monkey_patch_commands(app,), ["2520-07"])
assert "Data for 2520-Jul" in result.stdout
@freeze_time("2020-06-24")
def test_datetime_annotation_is_datetime(
):
patch_annotations(datetime_annotated)
assert datetime.datetime == datetime_annotated.__annotations__['some_date']
To use from datetime import datetime
from typing import Callable, Dict
import typer
def patch_annotations(function: Callable):
annotations: Dict = function.__annotations__
new_annotations = {}
for key, value in annotations.items():
# This if below is redundant
if value == datetime:
print("That would be impressive")
else:
print("Yes, as expected")
# Since above is always in the `else` department, we have to find other way to determine
# the type
if str(value) == "<class 'datetime.datetime'>":
value = datetime
new_annotations |= {key: value}
function.__annotations__ = new_annotations
def monkey_patch_commands(some_app: typer.Typer) -> typer.Typer:
registered_commands = some_app.registered_commands
for command in registered_commands:
print(command.callback.__annotations__)
patch_annotations(command.callback)
return some_app You can further improve the snippet by handling the |
Beta Was this translation helpful? Give feedback.
-
Hey there! Sorry for the delay. Yep, that's not a supported type, it's something dependent on that package (I haven't used it), nevertheless, I think you can probably do it with a custom type, this is somewhat new: https://typer.tiangolo.com/tutorial/parameter-types/custom-types/ |
Beta Was this translation helpful? Give feedback.
Hey there! Sorry for the delay.
Yep, that's not a supported type, it's something dependent on that package (I haven't used it), nevertheless, I think you can probably do it with a custom type, this is somewhat new: https://typer.tiangolo.com/tutorial/parameter-types/custom-types/