From 7bb5a0948c041d7ab2542a1c30b0da5fbcd87886 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 6 Sep 2023 10:40:28 -0300 Subject: [PATCH 01/15] FIX: improve optional dependency imports --- rocketpy/EnvironmentAnalysis.py | 59 ++++++++++++++------ rocketpy/plots/environment_analysis_plots.py | 9 +-- rocketpy/tools.py | 46 +++++++++++++-- 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index c661ebca7..935679bfe 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -10,7 +10,6 @@ import json from collections import defaultdict -import matplotlib.ticker as mtick import netCDF4 import numpy as np import pytz @@ -19,23 +18,13 @@ from rocketpy.Function import Function from rocketpy.units import convert_units -try: - import ipywidgets as widgets - import jsonpickle - from timezonefinder import TimezoneFinder - from windrose import WindroseAxes -except ImportError as error: - raise ImportError( - f"The following error was encountered while importing dependencies: '{error}'. " - "Please note that the EnvironmentAnalysis requires additional dependencies, " - "which can be installed by running 'pip install rocketpy[env_analysis]'." - ) from .plots.environment_analysis_plots import _EnvironmentAnalysisPlots from .prints.environment_analysis_prints import _EnvironmentAnalysisPrints from .tools import ( bilinear_interpolation, geopotential_to_height_agl, geopotential_to_height_asl, + import_optional_dependency, time_num_to_date_string, ) @@ -194,6 +183,9 @@ def __init__( self.unit_system = unit_system self.max_expected_altitude = max_expected_altitude + # Check if extra requirements are installed + self.__check_extra_requirements() + # Manage units and timezones self.__init_data_parsing_units() self.__find_preferred_timezone() @@ -232,6 +224,31 @@ def __init__( # Private, auxiliary methods + def __check_extra_requirements(self): + """Check if extra requirements are installed. If not, print a message + informing the user that some methods may not work and how to install + the extra requirements for environment analysis. + + Returns + ------- + None + """ + env_analysis_require = [ + "timezonefinder", + "windrose>=1.6.8", + "ipywidgets>=7.6.3", + "jsonpickle", + ] + for requirement in env_analysis_require: + try: + module = import_optional_dependency(requirement) + except ImportError: + print( + f"{requirement:20} is not installed. Some methods may not work." + + " You can install it by running 'pip install rocketpy[env_analysis]'" + ) + return None + def __init_surface_dictionary(self): # Create dictionary of file variable names to process surface data return { @@ -382,10 +399,18 @@ def __localize_input_dates(self): def __find_preferred_timezone(self): if self.preferred_timezone is None: # Use local timezone based on lat lon pair - tf = TimezoneFinder() - self.preferred_timezone = pytz.timezone( - tf.timezone_at(lng=self.longitude, lat=self.latitude) - ) + try: + timezonefinder = import_optional_dependency("timezonefinder") + tf = timezonefinder.TimezoneFinder() + self.preferred_timezone = pytz.timezone( + tf.timezone_at(lng=self.longitude, lat=self.latitude) + ) + except ImportError: + print( + "'timezonefinder' not installed, defaulting to UTC." + + " Install timezonefinder to get local time zone." + ) + self.preferred_timezone = pytz.timezone("UTC") elif isinstance(self.preferred_timezone, str): self.preferred_timezone = pytz.timezone(self.preferred_timezone) @@ -2727,6 +2752,7 @@ def load(self, filename="env_analysis_dict"): EnvironmentAnalysis object """ + jsonpickle = import_optional_dependency("jsonpickle") encoded_class = open(filename).read() return jsonpickle.decode(encoded_class) @@ -2743,6 +2769,7 @@ def save(self, filename="env_analysis_dict"): ------- None """ + jsonpickle = import_optional_dependency("jsonpickle") encoded_class = jsonpickle.encode(self) file = open(filename, "w") file.write(encoded_class) diff --git a/rocketpy/plots/environment_analysis_plots.py b/rocketpy/plots/environment_analysis_plots.py index ffd5491ef..4ffa2cd03 100644 --- a/rocketpy/plots/environment_analysis_plots.py +++ b/rocketpy/plots/environment_analysis_plots.py @@ -2,7 +2,6 @@ __copyright__ = "Copyright 20XX, RocketPy Team" __license__ = "MIT" -import ipywidgets as widgets import matplotlib.pyplot as plt import matplotlib.ticker as mtick import numpy as np @@ -11,11 +10,10 @@ from matplotlib.animation import FuncAnimation from matplotlib.animation import PillowWriter as ImageWriter from scipy import stats -from windrose import WindroseAxes from rocketpy.units import convert_units -from ..tools import find_two_closest_integers +from ..tools import find_two_closest_integers, import_optional_dependency # TODO: `wind_speed_limit` and `clear_range_limits` and should be numbers, not booleans @@ -856,6 +854,8 @@ def plot_wind_rose( ------- WindroseAxes """ + windrose = import_optional_dependency("windrose") + WindroseAxes = windrose.WindroseAxes ax = WindroseAxes.from_ax(fig=fig, rect=rect) ax.bar( wind_direction, @@ -990,8 +990,9 @@ def animate_average_wind_rose(self, figsize=(5, 5), filename="wind_rose.gif"): Returns ------- - Image : ipywidgets.widgets.widget_media.Image + Image : ipywidgets.widget_media.Image """ + widgets = import_optional_dependency("ipywidgets") metadata = dict( title="windrose", artist="windrose", diff --git a/rocketpy/tools.py b/rocketpy/tools.py index 3a4f97270..54a853012 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -1,12 +1,12 @@ -from itertools import product +import importlib +from bisect import bisect_left from cmath import isclose +from itertools import product -_NOT_FOUND = object() - -import numpy as np import pytz from cftime import num2pydate -from bisect import bisect_left + +_NOT_FOUND = object() class cached_property: @@ -1231,6 +1231,42 @@ def find_closest(ordered_sequence, value): return pivot_index - 1 if value - smaller <= greater - value else pivot_index +def import_optional_dependency( + name, +): + """Import an optional dependency. + + github.com/pandas-dev/pandas/blob/main/pandas/compat/_optional.py + If the dependency is not installed, an ImportError is raised. + + Parameters + ---------- + name : str + The name of the module to import. Can be used to import submodules too. + The name will be used as an argument to importlib.import_module method. + + Examples: + --------- + >>> from rocketpy.tools import import_optional_dependency + >>> matplotlib = import_optional_dependency("matplotlib") + >>> matplotlib.__name__ + 'matplotlib' + >>> plt = import_optional_dependency("matplotlib.pyplot") + >>> plt.__name__ + 'matplotlib.pyplot' + """ + package_name = name.split(".")[0] + try: + module = importlib.import_module(name) + except ImportError as exc: + raise ImportError( + f"{package_name} is an optional dependency and is not installed.\n" + + f"\t\tUse 'pip install {package_name}' to install it or " + + "'pip install rocketpy[all]' to install all optional dependencies." + ) from exc + return module + + if __name__ == "__main__": import doctest From bc73fcd776e758ae8e63b97b5ea3befdfc278870 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 6 Sep 2023 10:48:00 -0300 Subject: [PATCH 02/15] MAINT: introducing requirements-optional.txt file --- requirements-optional.txt | 4 ++++ requirements.txt | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 requirements-optional.txt diff --git a/requirements-optional.txt b/requirements-optional.txt new file mode 100644 index 000000000..814a7c1ab --- /dev/null +++ b/requirements-optional.txt @@ -0,0 +1,4 @@ +windrose>=1.6.8 +ipywidgets>=7.6.3 +jsonpickle +timezonefinder \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9b7619ead..18463c5d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,11 +2,6 @@ numpy>=1.13 scipy>=1.0 matplotlib>=3.0 netCDF4>=1.6.4 -windrose>=1.6.8 -ipywidgets>=7.6.3 requests pytz -timezonefinder simplekml -jsonpickle -numericalunits From e690da63c5d8b630e305558d9f544f6c4de657ab Mon Sep 17 00:00:00 2001 From: Guilherme Date: Wed, 6 Sep 2023 13:30:44 -0300 Subject: [PATCH 03/15] MAINT: improve dependency management function Update rocketpy/tools.py Co-authored-by: Pedro Henrique Marinho Bressan <87212571+phmbressan@users.noreply.github.com> --- rocketpy/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketpy/tools.py b/rocketpy/tools.py index 54a853012..f189dc8e9 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -1255,10 +1255,10 @@ def import_optional_dependency( >>> plt.__name__ 'matplotlib.pyplot' """ - package_name = name.split(".")[0] try: module = importlib.import_module(name) except ImportError as exc: + package_name = name.split(".")[0] raise ImportError( f"{package_name} is an optional dependency and is not installed.\n" + f"\t\tUse 'pip install {package_name}' to install it or " From 7cf2435ed069f331608bafd471081888d3e40ad4 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 6 Sep 2023 13:35:17 -0300 Subject: [PATCH 04/15] FIX: applying comments reviews --- rocketpy/EnvironmentAnalysis.py | 1 + rocketpy/tools.py | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index 935679bfe..9abf2e694 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -409,6 +409,7 @@ def __find_preferred_timezone(self): print( "'timezonefinder' not installed, defaulting to UTC." + " Install timezonefinder to get local time zone." + + " To do so, run 'pip install timezonefinder'" ) self.preferred_timezone = pytz.timezone("UTC") elif isinstance(self.preferred_timezone, str): diff --git a/rocketpy/tools.py b/rocketpy/tools.py index 54a853012..5e33be04d 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -1231,13 +1231,11 @@ def find_closest(ordered_sequence, value): return pivot_index - 1 if value - smaller <= greater - value else pivot_index -def import_optional_dependency( - name, -): - """Import an optional dependency. - +def import_optional_dependency(name): + """Import an optional dependency. If the dependency is not installed, an + ImportError is raised. This function is based on the implementation found in + pandas repository: github.com/pandas-dev/pandas/blob/main/pandas/compat/_optional.py - If the dependency is not installed, an ImportError is raised. Parameters ---------- From b048e247b8844c46a17cc6995de6c95ea380b437 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 6 Sep 2023 18:22:42 -0300 Subject: [PATCH 05/15] FIX: __check_extra_requirements() not working --- rocketpy/EnvironmentAnalysis.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index 9abf2e694..b4ed91677 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -239,9 +239,13 @@ def __check_extra_requirements(self): "ipywidgets>=7.6.3", "jsonpickle", ] + operators = [">=", "<=", "==", ">", "<"] for requirement in env_analysis_require: + pckg_name = requirement + for op in operators: + pckg_name = pckg_name.split(op)[0] try: - module = import_optional_dependency(requirement) + _ = import_optional_dependency(pckg_name) except ImportError: print( f"{requirement:20} is not installed. Some methods may not work." From 04f6a54b7f1b35281445b21c1c65259cac66442d Mon Sep 17 00:00:00 2001 From: Lint Action Date: Wed, 6 Sep 2023 21:23:25 +0000 Subject: [PATCH 06/15] Fix code style issues with Black --- rocketpy/EnvironmentAnalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index b4ed91677..337744ead 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -243,7 +243,7 @@ def __check_extra_requirements(self): for requirement in env_analysis_require: pckg_name = requirement for op in operators: - pckg_name = pckg_name.split(op)[0] + pckg_name = pckg_name.split(op)[0] try: _ = import_optional_dependency(pckg_name) except ImportError: From 174663b89da6396707c12ae1f7f356e5baad5060 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 6 Sep 2023 22:54:59 -0300 Subject: [PATCH 07/15] MAINT: update github workflows files - no longer assigns the issue to its author (only for PRs) - deleted the useless auto-assign-projects file - runs the pytest without the [all] option --- .github/auto-assign.yml | 7 +++++-- .github/workflows/auto-assign-projects | 21 --------------------- .github/workflows/auto-assign.yml | 4 +--- .github/workflows/test_pytest.yaml | 4 ++-- 4 files changed, 8 insertions(+), 28 deletions(-) delete mode 100644 .github/workflows/auto-assign-projects diff --git a/.github/auto-assign.yml b/.github/auto-assign.yml index 740c50523..5b0b21e2f 100644 --- a/.github/auto-assign.yml +++ b/.github/auto-assign.yml @@ -4,12 +4,13 @@ addReviewers: true # Set to 'author' to add PR's author as a assignee addAssignees: author -# A list of reviewers to be added to PRs (GitHub user name) +# A list of reviewers to be added to PRs (GitHub user name) reviewers: - Gui-FernandesBR - giovaniceotto - MateusStano - + - phmbressan + # A number of reviewers added to the PR # Set 0 to add all the reviewers (default: 0) numberOfReviewers: 0 @@ -17,3 +18,5 @@ numberOfReviewers: 0 # A list of keywords to be skipped the process if PR's title include it skipKeywords: - wip + - work in progress + - draft diff --git a/.github/workflows/auto-assign-projects b/.github/workflows/auto-assign-projects deleted file mode 100644 index 0c75b7aed..000000000 --- a/.github/workflows/auto-assign-projects +++ /dev/null @@ -1,21 +0,0 @@ -name: Auto Assign to Project(s) - -on: - issues: - types: [opened] - pull_request: - types: [opened] -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - assign_one_project: - runs-on: ubuntu-latest - name: Assign to One Project - steps: - - name: Assign NEW issues and NEW pull requests to RocketPy's main project - uses: srggrs/assign-one-project-github-action@1.3.1 - if: github.event.action == 'opened' - with: - project: 'https://github.com/orgs/RocketPy-Team/projects/1' - column_name: '🆕 New' diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 122a3107d..c58140c0d 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -1,7 +1,5 @@ -name: Auto Assign Issues and PRs once opened +name: Auto Assign PRs once opened on: - issues: - types: [opened] pull_request: types: [opened] jobs: diff --git a/.github/workflows/test_pytest.yaml b/.github/workflows/test_pytest.yaml index c06c3d1f6..615485b24 100644 --- a/.github/workflows/test_pytest.yaml +++ b/.github/workflows/test_pytest.yaml @@ -24,9 +24,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements-tests.txt - - name: Build RocketPy + - name: Build RocketPy (without optional dependencies) run: | - pip install -e.[all] + pip install -e. - name: Test with pytest run: | pytest From 0413472b427b905f60642940b2189c8cd532214f Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 7 Sep 2023 11:30:04 -0300 Subject: [PATCH 08/15] FIX: adding IPython as an optional require --- docs/user/requirements.rst | 1 + requirements-optional.txt | 1 + rocketpy/EnvironmentAnalysis.py | 9 +++++---- rocketpy/plots/environment_analysis_plots.py | 11 ++++++++++- rocketpy/tools.py | 6 +++++- setup.py | 1 + 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/user/requirements.rst b/docs/user/requirements.rst index a5edf83e9..3e56829ae 100644 --- a/docs/user/requirements.rst +++ b/docs/user/requirements.rst @@ -65,6 +65,7 @@ In case you want to use this class, you will need to install the following packa - `timezonefinder` : to allow for automatic timezone detection, - `windrose` : to allow for windrose plots, +- `ipython` : to allow for animated plots, - `ipywidgets` : to allow for GIFs generation, - `jsonpickle` : to allow for saving and loading of class instances. diff --git a/requirements-optional.txt b/requirements-optional.txt index 814a7c1ab..3709281dc 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,4 +1,5 @@ windrose>=1.6.8 +ipython>=8.14.0 ipywidgets>=7.6.3 jsonpickle timezonefinder \ No newline at end of file diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index 337744ead..500661669 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -7,6 +7,7 @@ import bisect import copy import datetime +import importlib.util import json from collections import defaultdict @@ -236,6 +237,7 @@ def __check_extra_requirements(self): env_analysis_require = [ "timezonefinder", "windrose>=1.6.8", + "IPython>=8.14.0", "ipywidgets>=7.6.3", "jsonpickle", ] @@ -244,11 +246,10 @@ def __check_extra_requirements(self): pckg_name = requirement for op in operators: pckg_name = pckg_name.split(op)[0] - try: - _ = import_optional_dependency(pckg_name) - except ImportError: + is_present = importlib.util.find_spec(pckg_name) + if is_present is None: print( - f"{requirement:20} is not installed. Some methods may not work." + f"{pckg_name:20} is not installed. Some methods may not work." + " You can install it by running 'pip install rocketpy[env_analysis]'" ) return None diff --git a/rocketpy/plots/environment_analysis_plots.py b/rocketpy/plots/environment_analysis_plots.py index 4ffa2cd03..773933d87 100644 --- a/rocketpy/plots/environment_analysis_plots.py +++ b/rocketpy/plots/environment_analysis_plots.py @@ -5,7 +5,6 @@ import matplotlib.pyplot as plt import matplotlib.ticker as mtick import numpy as np -from IPython.display import HTML from matplotlib import pyplot as plt from matplotlib.animation import FuncAnimation from matplotlib.animation import PillowWriter as ImageWriter @@ -1114,6 +1113,9 @@ def animate_wind_gust_distribution(self): HTML : IPython.core.display.HTML The animation as an HTML object """ + module = import_optional_dependency("IPython.display") + HTML = module.HTML # this is a class + # Gather animation data wind_gusts = self.env_analysis.surface_wind_gust_by_hour @@ -1312,6 +1314,9 @@ def animate_surface_wind_speed_distribution(self, wind_speed_limit=False): ------- HTML : IPython.core.display.HTML """ + module = import_optional_dependency("IPython.display") + HTML = module.HTML # this is a class + # Gather animation data surface_wind_speeds_at_given_hour = self.env_analysis.surface_wind_speed_by_hour @@ -1609,6 +1614,8 @@ def animate_wind_speed_profile(self, clear_range_limits=False): Whether to clear the sky range limits or not, by default False. This is useful when the launch site is constrained in terms or altitude. """ + module = import_optional_dependency("IPython.display") + HTML = module.HTML # this is a class # Create animation fig, ax = plt.subplots(dpi=200) @@ -1688,6 +1695,8 @@ def animate_wind_heading_profile(self, clear_range_limits=False): Whether to clear the sky range limits or not, by default False. This is useful when the launch site is constrained in terms or altitude. """ + module = import_optional_dependency("IPython.display") + HTML = module.HTML # this is a class # Create animation fig, ax = plt.subplots(dpi=200) diff --git a/rocketpy/tools.py b/rocketpy/tools.py index ba8798e2c..7e31280eb 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -8,6 +8,9 @@ _NOT_FOUND = object() +# Mapping of module name and the name of the package that should be installed +INSTALL_MAPPING = {"IPython": "ipython"} + class cached_property: def __init__(self, func): @@ -1256,7 +1259,8 @@ def import_optional_dependency(name): try: module = importlib.import_module(name) except ImportError as exc: - package_name = name.split(".")[0] + module_name = name.split(".")[0] + package_name = INSTALL_MAPPING.get(module_name, module_name) raise ImportError( f"{package_name} is an optional dependency and is not installed.\n" + f"\t\tUse 'pip install {package_name}' to install it or " diff --git a/setup.py b/setup.py index 24dd36f17..065d1e7df 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ env_analysis_require = [ "timezonefinder", "windrose>=1.6.8", + "IPython>=8.14.0", "ipywidgets>=7.6.3", "jsonpickle", ] From bcccebc9f89570901d2e905fa0db12f71b2941c5 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 7 Sep 2023 11:36:06 -0300 Subject: [PATCH 09/15] FIX: restore ipython version to 8.8 to allow py3.8 --- rocketpy/EnvironmentAnalysis.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index 500661669..a3da34ae7 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -237,7 +237,7 @@ def __check_extra_requirements(self): env_analysis_require = [ "timezonefinder", "windrose>=1.6.8", - "IPython>=8.14.0", + "IPython>=8.8.0", "ipywidgets>=7.6.3", "jsonpickle", ] diff --git a/setup.py b/setup.py index 065d1e7df..d875a7c0a 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ env_analysis_require = [ "timezonefinder", "windrose>=1.6.8", - "IPython>=8.14.0", + "IPython>=8.8.0", "ipywidgets>=7.6.3", "jsonpickle", ] From d032296266ea5ec32953987d36b58333d66719ad Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 7 Sep 2023 12:19:37 -0300 Subject: [PATCH 10/15] TST: apply import_optional_dependency to tests --- tests/test_environment_analysis.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_environment_analysis.py b/tests/test_environment_analysis.py index a09d48e00..7b5d9570c 100644 --- a/tests/test_environment_analysis.py +++ b/tests/test_environment_analysis.py @@ -2,10 +2,10 @@ import os from unittest.mock import patch -import ipywidgets as widgets import matplotlib as plt import pytest -from IPython.display import HTML + +from rocketpy.tools import import_optional_dependency plt.rcParams.update({"figure.max_open_warning": 0}) @@ -146,6 +146,9 @@ def test_animation_plots(mock_show, env_analysis): env_analysis : EnvironmentAnalysis A simple object of the EnvironmentAnalysis class. """ + # import dependencies + widgets = import_optional_dependency("ipywidgets") + HTML = import_optional_dependency("IPython.display").HTML # Check animation plots assert isinstance(env_analysis.plots.animate_average_wind_rose(), widgets.Image) From 9ddcec928a2e7fb5e7bfafb2f229a90a9a5796b1 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 7 Sep 2023 12:35:01 -0300 Subject: [PATCH 11/15] TST: test the import rocketpy --- .github/workflows/test_pytest.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test_pytest.yaml b/.github/workflows/test_pytest.yaml index 615485b24..e0a6e9325 100644 --- a/.github/workflows/test_pytest.yaml +++ b/.github/workflows/test_pytest.yaml @@ -27,6 +27,9 @@ jobs: - name: Build RocketPy (without optional dependencies) run: | pip install -e. + - name: Import rocketpy in python and test if it works + run: | + python -c "import sys, rocketpy; print(f'{rocketpy.__name__} running on Python {sys.version}')" - name: Test with pytest run: | pytest From 979a5c719ebb0a335d951c0a3bd9c520901405d8 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 10 Sep 2023 01:00:57 -0300 Subject: [PATCH 12/15] FIX: improve extra req checks --- requirements-optional.txt | 2 +- rocketpy/EnvironmentAnalysis.py | 42 ++++++++++++++++-------------- rocketpy/tools.py | 46 +++++++++++++++++++++++++++++++++ setup.py | 2 +- 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 3709281dc..699bbde14 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,5 +1,5 @@ windrose>=1.6.8 -ipython>=8.14.0 +ipython ipywidgets>=7.6.3 jsonpickle timezonefinder \ No newline at end of file diff --git a/rocketpy/EnvironmentAnalysis.py b/rocketpy/EnvironmentAnalysis.py index a3da34ae7..172402d3a 100644 --- a/rocketpy/EnvironmentAnalysis.py +++ b/rocketpy/EnvironmentAnalysis.py @@ -7,7 +7,6 @@ import bisect import copy import datetime -import importlib.util import json from collections import defaultdict @@ -23,6 +22,7 @@ from .prints.environment_analysis_prints import _EnvironmentAnalysisPrints from .tools import ( bilinear_interpolation, + check_requirement_version, geopotential_to_height_agl, geopotential_to_height_asl, import_optional_dependency, @@ -185,7 +185,7 @@ def __init__( self.max_expected_altitude = max_expected_altitude # Check if extra requirements are installed - self.__check_extra_requirements() + self.__check_requirements() # Manage units and timezones self.__init_data_parsing_units() @@ -225,7 +225,7 @@ def __init__( # Private, auxiliary methods - def __check_extra_requirements(self): + def __check_requirements(self): """Check if extra requirements are installed. If not, print a message informing the user that some methods may not work and how to install the extra requirements for environment analysis. @@ -234,24 +234,28 @@ def __check_extra_requirements(self): ------- None """ - env_analysis_require = [ - "timezonefinder", - "windrose>=1.6.8", - "IPython>=8.8.0", - "ipywidgets>=7.6.3", - "jsonpickle", - ] - operators = [">=", "<=", "==", ">", "<"] - for requirement in env_analysis_require: - pckg_name = requirement - for op in operators: - pckg_name = pckg_name.split(op)[0] - is_present = importlib.util.find_spec(pckg_name) - if is_present is None: + env_analysis_require = { # The same as in the setup.py file + "timezonefinder": "", + "windrose": ">=1.6.8", + "IPython": "", + "ipywidgets": ">=7.6.3", + "jsonpickle": "", + } + has_error = False + for module_name, version in env_analysis_require.items(): + version = ">=0" if not version else version + try: + check_requirement_version(module_name, version) + except (ValueError, ImportError) as e: + has_error = True print( - f"{pckg_name:20} is not installed. Some methods may not work." - + " You can install it by running 'pip install rocketpy[env_analysis]'" + f"The following error occurred while importing {module_name}: {e}" ) + if has_error: + print( + "Given the above errors, some methods may not work. Please run " + + "'pip install rocketpy[env_analysis]' to install extra requirements." + ) return None def __init_surface_dictionary(self): diff --git a/rocketpy/tools.py b/rocketpy/tools.py index 7e31280eb..e72b07f3a 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -1,6 +1,8 @@ import importlib +import re from bisect import bisect_left from cmath import isclose +from importlib.metadata import version as importlib_get_version from itertools import product import pytz @@ -1269,6 +1271,50 @@ def import_optional_dependency(name): return module +def check_requirement_version(module_name, version): + """This function tests if a module is installed and if the version is + correct. If the module is not installed, an ImportError is raised. If the + version is not correct, an error is raised. + + Parameters + ---------- + module_name : str + The name of the module to be tested. + version : str + The version of the module that is required. The string must start with + one of the following operators: ">", "<", ">=", "<=", "==", "!=". + + Example: + -------- + >>> from rocketpy.tools import check_requirement_version + >>> check_requirement_version("numpy", ">=1.0.0") + True + >>> check_requirement_version("matplotlib", ">=3.0") + True + """ + operators = [">=", "<=", "==", ">", "<", "!="] + # separator the operator from the version number + operator, v_number = re.match(f"({'|'.join(operators)})(.*)", version).groups() + + if operator not in operators: + raise ValueError( + f"Version must start with one of the following operators: {operators}" + ) + if importlib.util.find_spec(module_name) is None: + raise ImportError( + f"{module_name} is not installed. You can install it by running " + + f"'pip install {module_name}'" + ) + installed_version = importlib_get_version(module_name) + if not eval(f'"{installed_version}" {operator} "{v_number}"'): + raise ImportError( + f"{module_name} version is {installed_version}, but version {version} " + + f"is required. You can install a correct version by running " + + f"'pip install {module_name}{version}'" + ) + return True + + if __name__ == "__main__": import doctest diff --git a/setup.py b/setup.py index d875a7c0a..e481e9822 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ env_analysis_require = [ "timezonefinder", "windrose>=1.6.8", - "IPython>=8.8.0", + "IPython", "ipywidgets>=7.6.3", "jsonpickle", ] From 53415480f94b7209a640c3dc6fe173816f69660e Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 10 Sep 2023 01:17:23 -0300 Subject: [PATCH 13/15] GIT: improve pytest workflow based on rev --- .github/workflows/test_pytest.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_pytest.yaml b/.github/workflows/test_pytest.yaml index e0a6e9325..8d71eb857 100644 --- a/.github/workflows/test_pytest.yaml +++ b/.github/workflows/test_pytest.yaml @@ -23,13 +23,15 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-tests.txt - name: Build RocketPy (without optional dependencies) run: | - pip install -e. + pip install . - name: Import rocketpy in python and test if it works run: | python -c "import sys, rocketpy; print(f'{rocketpy.__name__} running on Python {sys.version}')" + - name: Install optional dependencies + run: | + pip install -r requirements-tests.txt - name: Test with pytest run: | pytest From 63823643e12fab3ca60b2b8cff744791410eb0d2 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Fri, 15 Sep 2023 13:57:27 -0300 Subject: [PATCH 14/15] MAINT: avoid using eval() --- rocketpy/tools.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rocketpy/tools.py b/rocketpy/tools.py index e72b07f3a..14f54139c 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -2,11 +2,11 @@ import re from bisect import bisect_left from cmath import isclose -from importlib.metadata import version as importlib_get_version from itertools import product import pytz from cftime import num2pydate +from packaging import version as packaging_version _NOT_FOUND = object() @@ -1305,12 +1305,13 @@ def check_requirement_version(module_name, version): f"{module_name} is not installed. You can install it by running " + f"'pip install {module_name}'" ) - installed_version = importlib_get_version(module_name) - if not eval(f'"{installed_version}" {operator} "{v_number}"'): + installed_version = packaging_version.parse(importlib.metadata.version(module_name)) + required_version = packaging_version.parse(v_number) + if installed_version < required_version: raise ImportError( - f"{module_name} version is {installed_version}, but version {version} " - + f"is required. You can install a correct version by running " - + f"'pip install {module_name}{version}'" + f"{module_name} version is {installed_version}, which is not correct" + + f". A version {version} is required. You can install a correct " + + f"version by running 'pip install {module_name}{version}'" ) return True From 96a8ffc1fca3fb6fdd448d3598bd1f04ba81c306 Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Fri, 15 Sep 2023 19:40:54 -0300 Subject: [PATCH 15/15] FIX: importlib metadata import error. --- rocketpy/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketpy/tools.py b/rocketpy/tools.py index 14f54139c..44df0d778 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -1,4 +1,4 @@ -import importlib +import importlib, importlib.metadata import re from bisect import bisect_left from cmath import isclose