diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index ffeb0434..fc69f702 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -5,11 +5,23 @@ on: - workflow_dispatch env: - EE_DECRYPT_KEY: ${{ secrets.EE_DECRYPT_KEY }} - PLANET_API_CREDENTIALS: '${{ secrets.PLANET_API_CREDENTIALS }}' + #EE_DECRYPT_KEY: ${{ secrets.EE_DECRYPT_KEY }} + PLANET_API_CREDENTIALS: ${{ secrets.PLANET_API_CREDENTIALS }} PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }} + EARTHENGINE_TOKEN: ${{ secrets.EARTHENGINE_TOKEN }} + #SKIP: isort jobs: + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.8" + - uses: pre-commit/action@v3.0.0 + build: runs-on: ubuntu-latest strategy: @@ -18,10 +30,10 @@ jobs: python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -37,15 +49,8 @@ jobs: - name: Install dependencies run: pip install .[test] - - name: Test formatting - uses: psf/black@stable - with: - version: "22.3.0" - - - name: PEP8 rules - uses: tonybajan/flake8-check-action@v1.0.0 - with: - repotoken: ${{ secrets.GITHUB_TOKEN }} + - name: Set up GEE credentials + run: ee_token - name: build the documentation if: matrix.python-version == '3.8' @@ -67,23 +72,25 @@ jobs: assert len(unexpected) == 0 - name: test with pytest - run: coverage run -m pytest --color=yes tests + run: pytest --color=yes --cov --cov-report=xml --instafail tests + + - name: assess dead fixtures + if: matrix.python-version == '3.8' + run: pytest --dead-fixtures - name: build the template panel application if: matrix.python-version == '3.8' - run: | - pytest --nbmake sepal_ui/templates/panel_app/ui.ipynb + run: pytest --nbmake sepal_ui/templates/panel_app/ui.ipynb - name: build the template map application if: matrix.python-version == '3.8' - run: | - pytest --nbmake sepal_ui/templates/map_app/ui.ipynb + run: pytest --nbmake sepal_ui/templates/map_app/ui.ipynb - name: coverage run: coverage xml - name: codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} verbose: true diff --git a/.gitignore b/.gitignore index c4646c58..d3888997 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # custom .coverage + ee_private_key.json warnings.txt diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a0619e12..315474a1 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -27,7 +27,7 @@ The tool is currently tranlated in the following languages: English, Français, Español -You can contribute to the translation effort on our `pontoon project `__. Contributors can suggest new languages and new translation. The admin will review this modification as fast as possible. If nobody in the core team master the suggested language, we'll be force to trust you ! +You can contribute to the translation effort on our `crowdin project `__. Contributors can suggest new languages and new translation. The admin will review this modification as fast as possible. If nobody in the core team master the suggested language, we'll be force to trust you ! Develop within the project @@ -35,17 +35,16 @@ Develop within the project Since 2020-08-14, this repository follows these `development guidelines `_. The git flow is thus the following: -.. figure:: https://nvie.com/img/git-model@2x.png - :alt: the Git branching model - :width: 70% +.. figure:: https://raw.githubusercontent.com/12rambau/sepal_ui/links/docs/source/_image/branching_system.png + :alt: the Git branching model The git branching model Please consider using the :code:`--no-ff` option when merging to keep the repository consistent with PR. -In the project to adapt to :code:`JupyterLab` IntelSense, we decided to explicitly write the `return` statement for every function. +In the project to adapt to :code:`JupyterLab` IntelSense, we decided to explicitly write the ``return`` statement for every function. -As we are holding a single documentation page, we need to provide the users with version informations. When a new function or class is created please use the `Deprecated `__ lib to specify that the feature is new in the documentation. +When a new function or class is created please use the `Deprecated `__ lib to specify that the feature is new in the documentation. .. code-block:: python @@ -79,13 +78,13 @@ You can learn more about Conventional Commits following this `link `_. +The CI should take everything in control from here and execute the :code:`Upload Python Package` GitHub Action that is publishing the new version on `PyPi `_. Once it's done you need to trigger the rebuild of SEPAL. modify the following `file `_ with the latest version number and the rebuild will start automatically. - -Setting ENV variables ---------------------- +ENV for Planet components +------------------------- Sometimes is useful to create enviromental variables to store some data that your workflows will receive (i.e. component testing). For example, to perform the local tests of the :code:`planetapi` sepal module, the :code:`PLANET_API_KEY` and :code:`PLANET_API_CREDENTIALS` env vars are required, even though they are also skippable. @@ -134,17 +132,39 @@ To store a variable in your local session, just type :code:`export=` followed by $ export PLANET_API_KEY="neverending_resourcesapi" -However, this variable will expire everytime you start a new session, to create it every session and make it live longer, go to your :code:`home` folder and save the previous line in the :code:`.bash_profile` file. +.. tip:: -.. code-block:: console + In SEPAL this variable will expire everytime you start a new session, to create it every session and make it live longer, go to your :code:`home` folder and save the previous line in the :code:`.bash_profile` file. + + .. code-block:: console + + $ vim .bash_profile - $ vim .bash_profile +The current enviromental keys and its structure is the following: -.. note:: - The current enviromental keys and its structure is the following: +- ``PLANET_API_CREDENTIALS='{"username": "user@neim.com", "password": "secure"}'`` +- ``PLANET_API_KEY="string_planet_api_key"`` + +ENV for GEE component +--------------------- - - PLANET_API_CREDENTIALS: '{"username": "user@neim.com", "password": "secure"}' - - PLANET_API_KEY: "string_planet_api_key" +To test/use the Google EarthEngine components, you need to run the `ìnit__ee`` script. + +In a local development environment you can fully rely on your own GEE account. simply make sure to run at least once the authentification process from a terminal: + +.. code-block:: console + + $ earthengine authenticate + +In a distant environment (such as GitHub Actions) it is compulsory to use a environment variable to link your earthengine account. First, find the Earth Engine credentials file on your computer. + +.. code-block:: + + Windows: C:\Users\USERNAME\.config\earthengine\credentials + Linux: /home/USERNAME/.config/earthengine/credentials + MacOS: /Users/USERNAME/.config/earthengine/credentials + +Open the credentials file and copy its content. On the **GitHub Actions** page, create a new **secret** with the name ``EARTHENGINE_TOKE``, and the value of the copied content. Build the API documentation files --------------------------------- diff --git a/README.rst b/README.rst index 0525463c..f8a55581 100644 --- a/README.rst +++ b/README.rst @@ -66,11 +66,18 @@ You can contribute to the translation effort on our `crowdin project `__. It is designed on top of the amazing `ipyvuetify `_ library and will help developer to easily create interface for their workflows. By using this libraries, you'll ensure a robust and unified interface for your scripts and a easy and complete integration into the SEPAL dashboard of application. -The full documentation is available `here `__ and a demo app can be launched on Heroku following this link: ``__. +The full documentation is available `here `__ and demo apps can be launched on Heroku following these links: + +- `Map style application `__ +- `Panel style application `__ We are happy to receive feedback and we welcome any kind of contribution. -.. image:: https://raw.githubusercontent.com/12rambau/sepal_ui/master/docs/source/_image/sepal_ui_screenshot.png +.. image:: https://raw.githubusercontent.com/12rambau/sepal_ui/links/docs/source/_image/demo-map-app.png + :width: 49% + +.. image:: https://raw.githubusercontent.com/12rambau/sepal_ui/links/docs/source/_image/demo-panel-app.png + :width: 49% Contribute ---------- diff --git a/docs/source/_image/branching_system.png b/docs/source/_image/branching_system.png new file mode 100644 index 00000000..7731f32d Binary files /dev/null and b/docs/source/_image/branching_system.png differ diff --git a/docs/source/_image/demo-map-app.png b/docs/source/_image/demo-map-app.png new file mode 100644 index 00000000..d29f09ab Binary files /dev/null and b/docs/source/_image/demo-map-app.png differ diff --git a/docs/source/_image/demo-panel-app.png b/docs/source/_image/demo-panel-app.png new file mode 100644 index 00000000..8132f1d0 Binary files /dev/null and b/docs/source/_image/demo-panel-app.png differ diff --git a/docs/source/_image/sepal_ui_screenshot.png b/docs/source/_image/sepal_ui_screenshot.png deleted file mode 100644 index c4efd3a7..00000000 Binary files a/docs/source/_image/sepal_ui_screenshot.png and /dev/null differ diff --git a/docs/source/conf.py b/docs/source/conf.py index 947e64cb..0f8ba541 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -111,12 +111,12 @@ { "name": "GitHub", "url": "https://github.com/12rambau/sepal_ui", - "icon": "fab fa-github", + "icon": "fa-brands fa-github", }, { "name": "Pypi", "url": "https://pypi.org/project/sepal-ui/", - "icon": "fab fa-python", + "icon": "fa-brands fa-python", }, ], } diff --git a/docs/source/index.rst b/docs/source/index.rst index ccffd6ad..8359dd90 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,11 +18,14 @@ You can contribute to the translation effort on our `crowdin project `_ library and will help developer to easily create interface for their workflows. By using this libraries, you'll ensure a robust and unified interface for your scripts and a easy and complete integration into the SEPAL dashboard of application. -A demo app can be launched on Heroku following this link: ``__. +Demo apps can be launched on Heroku following these links: + +- `Map style app `__ +- `Panel style app `__ We are happy to receive feedback and we welcome any kind of contribution. -.. image:: https://raw.githubusercontent.com/12rambau/sepal_ui/master/docs/source/_image/sepal_ui_demo.gif +.. image:: https://raw.githubusercontent.com/12rambau/sepal_ui/main/docs/source/_image/sepal_ui_demo.gif Contribute ---------- diff --git a/docs/source/start/index.rst b/docs/source/start/index.rst index 5c21dbba..71b36069 100644 --- a/docs/source/start/index.rst +++ b/docs/source/start/index.rst @@ -4,11 +4,14 @@ Getting started :code:`sepal_ui` is a lib designed to create elegant python based dashboard in the SEPAL environment. It is designed on top of the amazing `ipyvuetify `_ library and will help developer to easily create interface for their workflows. By using this libraries, you'll ensure a robust and unified interface for your scripts and a easy and complete integration into the SEPAL dashboard of application. -The full documentation is available `here `__ and a demo app can be launched on Heroku following this link: ``__. +The full documentation is available `here `__ and a demo apps can be launched on Heroku following these links: + +- `Map style application `__ +- `Panel style application `__ We are happy to receive feedback and we welcome any kind of contribution. -.. image:: https://raw.githubusercontent.com/12rambau/sepal_ui/master/docs/source/_image/sepal_ui_demo.gif +.. image:: https://raw.githubusercontent.com/12rambau/sepal_ui/main/docs/source/_image/sepal_ui_demo.gif Table of content ---------------- diff --git a/docs/source/tutorials/add-tile.rst b/docs/source/tutorials/add-tile.rst index f2c75b06..91524bc2 100644 --- a/docs/source/tutorials/add-tile.rst +++ b/docs/source/tutorials/add-tile.rst @@ -135,7 +135,7 @@ in the :code:`app_items` list, add a :code:`DrawerItem` corresponding to your ti # [...] sw.DrawerItem( title = ms.app.drawer_item.aoi, - icon 'fas fa-cogs', # optional + icon 'fa-solid fa-cogs', # optional card="my_tile" ) ] diff --git a/docs/source/tutorials/custom-widget.rst b/docs/source/tutorials/custom-widget.rst index cfd95d52..7d392492 100644 --- a/docs/source/tutorials/custom-widget.rst +++ b/docs/source/tutorials/custom-widget.rst @@ -69,7 +69,7 @@ Here we will create the object with its expected attributes def __init__(self, label="Password", **kwargs): # create the eye icon - self.eye = v.Icon(class_ = 'ml-1', children=['fas fa-eye']) + self.eye = v.Icon(class_ = 'ml-1', children=['fa-solid fa-eye']) # create the texfied self.text_field = v.TextField( @@ -123,7 +123,7 @@ Toggle visibility Now we want to add a behavior to our object. When we click on the eye, the :code:`PasswordField` should toggle its visibility: -* The eye should switch from :code:`fas fa-eye` and :code:`fas fa-eye-slash` +* The eye should switch from :code:`fa-solid fa-eye` and :code:`fa-solid fa-eye-slash` * The text_field should switch from type :code:`password` to :code:`text` To do so we will first add 2 class static variable (caps lock) to list the 2 types and icon and set them on the two attributes of my class. a new attribute needs to be created to remind the current state of the password. @@ -138,7 +138,7 @@ I'll call it :code:`password_viz` as the :code:`viz` parameter is already an att class PasswordField(sw.SepalWidget, v.Layout): - EYE_ICONS = ['fas fa-eye', 'fas fa-eye-slash'] # new icon list + EYE_ICONS = ['fa-solid fa-eye', 'fa-solid fa-eye-slash'] # new icon list TYPES = ['password', 'text'] # new type list def __init__(self, label="Password", **kwargs): @@ -224,7 +224,7 @@ finally we obtain the following reusable widget : class PasswordField(sw.SepalWidget, v.Layout): - EYE_ICONS = ['fas fa-eye', 'fas fa-eye-slash'] # new icon list + EYE_ICONS = ['fa-solid fa-eye', 'fa-solid fa-eye-slash'] # new icon list TYPES = ['password', 'text'] # new type list def __init__(self, label="Password", **kwargs): diff --git a/docs/source/widgets/btn.rst b/docs/source/widgets/btn.rst index 949d5468..94eafeff 100644 --- a/docs/source/widgets/btn.rst +++ b/docs/source/widgets/btn.rst @@ -5,7 +5,7 @@ Overview -------- :code:`Btn` is custom widget to provide easy to use button in the sepal_ui framework. it inherits from the :code:`SepalWidget` class. -Any argument from the original :code:`Btn` ipyvuetify class can be used to complement it. The button icon needs to be searched in the `fontAwesome library `__ or mdi library `_, if none is set, a :code:`fas fa-check` will be used. +Any argument from the original :code:`Btn` ipyvuetify class can be used to complement it. The button icon needs to be searched in the `fontAwesome library `__ or mdi library `_, if none is set, a :code:`fa-solid fa-check` will be used. The default color is set to "primary". .. jupyter-execute:: @@ -20,8 +20,8 @@ The default color is set to "primary". v.theme.dark = False btn = sw.Btn( - text = "The One btn", - icon = "fas fa-cogs" + msg = "The One btn", + gliph = "fa-solid fa-cogs" ) btn @@ -42,8 +42,8 @@ Btn can be used to launch function on any Javascript event such as "click". v.theme.dark = False btn = sw.Btn( - text = "The One btn", - icon = "fas fa-cogs" + msg = "The One btn", + gliph = "fa-solid fa-cogs" ) btn.on_event('click', lambda *args: print('Hello world!')) diff --git a/sepal_ui/aoi/aoi_model.py b/sepal_ui/aoi/aoi_model.py index 5c58b0fd..3262a84a 100644 --- a/sepal_ui/aoi/aoi_model.py +++ b/sepal_ui/aoi/aoi_model.py @@ -157,7 +157,7 @@ class AoiModel(Model): reason=":code:`alert` positional argument will be removed. Successfull output messages has to be created in AoiView.", ) def __init__( - self, alert=None, gee=True, vector=None, admin=None, asset=None, folder=None + self, alert=None, gee=True, vector=None, admin=None, asset=None, folder="" ): super().__init__() @@ -166,7 +166,7 @@ def __init__( self.ee = gee if gee: su.init_ee() - self.folder = folder or ee.data.getAssetRoots()[0]["id"] + self.folder = str(folder) or ee.data.getAssetRoots()[0]["id"] # set default values self.set_default(vector, admin, asset) @@ -620,8 +620,8 @@ def get_ipygeojson(self): style.update(color=color.success, fillColor=color.success) # create a GeoJSON object - self.ipygeojson = GeoJSON( - data=data, style=style, name="aoi", attribution="SEPAL(c)" - ) + # attribution="SEPAL(c)" is not recognized yet + # https://github.com/jupyter-widgets/ipyleaflet/issues/847 + self.ipygeojson = GeoJSON(data=data, style=style, name="aoi") return self.ipygeojson diff --git a/sepal_ui/aoi/aoi_view.py b/sepal_ui/aoi/aoi_view.py index 2e439fc3..124971b4 100644 --- a/sepal_ui/aoi/aoi_view.py +++ b/sepal_ui/aoi/aoi_view.py @@ -5,8 +5,10 @@ from traitlets import Int import sepal_ui.sepalwidgets as sw +from sepal_ui import mapping as sm from sepal_ui.aoi.aoi_model import AoiModel from sepal_ui.message import ms +from sepal_ui.scripts import decorator as sd from sepal_ui.scripts import utils as su CUSTOM = AoiModel.CUSTOM @@ -138,7 +140,7 @@ def get_items(self, filter_=None): update the item list based on the given filter Params: - filter\_ (str): The code of the parent v_model to filter the current results + filter\\_ (str): The code of the parent v_model to filter the current results Return: self @@ -187,7 +189,7 @@ class AoiView(sw.Card): Args: methods (list, optional): the methods to use in the widget, default to 'ALL'. Available: {'ADMIN0', 'ADMIN1', 'ADMIN2', 'SHAPE', 'DRAW', 'POINTS', 'ASSET', 'ALL'} - map\_ (SepalMap, optional): link the aoi_view to a custom SepalMap to display the output, default to None + map\\_ (SepalMap, optional): link the aoi_view to a custom SepalMap to display the output, default to None gee (bool, optional): wether to bind to ee or not vector (str|pathlib.Path, optional): the path to the default vector object admin (int, optional): the administrative code of the default selection. Need to be GADM if :code:`ee==False` and GAUL 2015 if :code:`ee==True`. @@ -217,6 +219,9 @@ class AoiView(sw.Card): map_ = None "sepal_ui.mapping.SepalMap: the map to draw the AOI" + aoi_dc = None + "sepal_ui.mapping.DrawControl: the drawing control associated with DRAW method" + w_method = None "widget: the widget to select the method" @@ -255,7 +260,7 @@ class AoiView(sw.Card): reason="Model is now an optional parameter to AoiView, it can be created from outside and passed to the initialization function.", ) def __init__( - self, methods="ALL", map_=None, gee=True, folder=None, model=None, **kwargs + self, methods="ALL", map_=None, gee=True, folder="", model=None, **kwargs ): # set ee dependencie @@ -278,7 +283,6 @@ def __init__( self.w_admin_2 = AdminField(2, self.w_admin_1, gee=gee) self.w_vector = sw.VectorField(label=ms.aoi_sel.vector) self.w_points = sw.LoadTableField(label=ms.aoi_sel.points) - self.w_draw = sw.TextField(label=ms.aoi_sel.aoi_name) # group them together with the same key as the select_method object self.components = { @@ -287,7 +291,6 @@ def __init__( "ADMIN2": self.w_admin_2, "SHAPE": self.w_vector, "POINTS": self.w_points, - "DRAW": self.w_draw, } # hide them all @@ -304,7 +307,6 @@ def __init__( .bind(self.w_vector, "vector_json") .bind(self.w_points, "point_json") .bind(self.w_method, "method") - .bind(self.w_draw, "name") ) # defint the asset select separately. If no gee is set up we don't want any @@ -313,10 +315,19 @@ def __init__( if self.ee: self.w_asset = sw.VectorField( label=ms.aoi_sel.asset, gee=True, folder=self.folder, types=["TABLE"] - ).hide() + ) + self.w_asset.hide() self.components["ASSET"] = self.w_asset self.model.bind(self.w_asset, "asset_name") + # define DRAW option separately as it will only work if the map is set + if self.map_: + self.w_draw = sw.TextField(label=ms.aoi_sel.aoi_name).hide() + self.components["DRAW"] = self.w_draw + self.model.bind(self.w_draw, "name") + self.aoi_dc = sm.DrawControl(self.map_) + self.aoi_dc.hide() + # add a validation btn self.btn = sw.Btn(ms.aoi_sel.btn) @@ -334,13 +345,13 @@ def __init__( # reset te aoi_model self.model.clear_attributes() - @su.loading_button(debug=True) + @sd.loading_button(debug=True) def _update_aoi(self, widget, event, data): """load the object in the model & update the map (if possible)""" # read the information from the geojson datas if self.map_: - self.model.geo_json = self.map_.dc.to_json() + self.model.geo_json = self.aoi_dc.to_json() # update the model self.model.set_object() @@ -356,7 +367,7 @@ def _update_aoi(self, widget, event, data): else: self.map_.add_layer(self.model.get_ipygeojson()) - self.map_.hide_dc() + self.aoi_dc.hide() # tell the rest of the apps that the aoi have been updated self.updated += 1 @@ -374,7 +385,7 @@ def reset(self): return self - @su.switch("loading", on_widgets=["w_method"]) + @sd.switch("loading", on_widgets=["w_method"]) def _activate(self, change): """activate the adapted widgets""" @@ -391,9 +402,9 @@ def _activate(self, change): # clear the geo_json saved features to start from scratch if self.map_: if change["new"] == "DRAW": - self.map_.dc.show() + self.aoi_dc.show() else: - self.map_.dc.hide() + self.aoi_dc.hide() # activate the correct widget w = next((w for k, w in self.components.items() if k == change["new"]), None) diff --git a/sepal_ui/bin/ee_token.py b/sepal_ui/bin/ee_token.py new file mode 100644 index 00000000..c2c7baec --- /dev/null +++ b/sepal_ui/bin/ee_token.py @@ -0,0 +1,61 @@ +#!/usr/bin/python3 + +""" +Script to create a GEE credential file from a environment variable. + +The script should be run in CD/CI environment as Github actions. It copy the credentials saved by maintainers in the "EARTHENGINE_TOKEN" environment variable and copy them to the appropriate file so that the test can be run. Note that the credentials have expiration dates so it will need to be changed on regular basis. Not intended for local dev as the user should be authenticated. +""" + +import argparse +import os +from pathlib import Path + +from colorama import Fore, init + +# init colors for all plateforms +init() + +# init parser +parser = argparse.ArgumentParser(description=__doc__, usage="ee_token") + + +def set_credentials(ee_token: str) -> None: + """ + Set the credentials of the earthengine account based on ee_token. + + Args: + ee_token: the str representation of existing GEE credentials + """ + + # write them in the appropriate file + credential_folder_path = Path.home() / ".config" / "earthengine" + credential_folder_path.mkdir(parents=True, exist_ok=True) + credential_file_path = credential_folder_path / "credentials" + with credential_file_path.open("w") as f: + f.write(ee_token) + + return + + +def main() -> None: + + # read arguments (there should be none) + parser.parse_args() + + # welcome the user + print("Creating credentials for your build environment\n\n") + + # check if the environment variable is available + ee_token = os.environ["EARTHENGINE_TOKEN"] + + # create the file + set_credentials(ee_token) + + # display one last message + print(f"{Fore.GREEN}The GEE credentials have been set.{Fore.RESET}") + + return + + +if __name__ == "__main__": + main() diff --git a/sepal_ui/frontend/json/file_icons.json b/sepal_ui/frontend/json/file_icons.json index 2b140fd1..4a4ec0fd 100644 --- a/sepal_ui/frontend/json/file_icons.json +++ b/sepal_ui/frontend/json/file_icons.json @@ -1,12 +1,12 @@ { - "": {"color": ["#ffca28", "#ffc107"], "icon": "far fa-folder"}, - ".csv": {"color": ["#4caf50", "#00c853"], "icon": "far fa-table"}, - ".txt": {"color": ["#4caf50", "#00c853"], "icon": "far fa-table"}, - ".tif": {"color": ["#9c27b0", "#673ab7"], "icon": "far fa-image"}, + "": {"color": ["#ffca28", "#ffc107"], "icon": "fa-regular fa-folder"}, + ".csv": {"color": ["#4caf50", "#00c853"], "icon": "fa-solid fa-table"}, + ".txt": {"color": ["#4caf50", "#00c853"], "icon": "fa-solid fa-table"}, + ".tif": {"color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image"}, ".tiff": {"color": ["#9c27b0", "#673ab7"], "icon": "far fa-image"}, - ".vrt": {"color": ["#9c27b0", "#673ab7"], "icon": "far fa-image"}, - ".shp": {"color": ["#9c27b0", "#673ab7"], "icon": "far fa-vector-square"}, - ".geojson": {"color": ["#9c27b0", "#673ab7"], "icon": "far fa-vector-square"}, - "DEFAULT": {"color": ["#00bcd4", "#03a9f4"], "icon": "far fa-file"}, - "PARENT": {"color": ["#424242", "#ffffff"], "icon": "far fa-folder-open"} + ".vrt": {"color": ["#9c27b0", "#673ab7"], "icon": "fa-regular fa-image"}, + ".shp": {"color": ["#9c27b0", "#673ab7"], "icon": "fa-solid fa-vector-square"}, + ".geojson": {"color": ["#9c27b0", "#673ab7"], "icon": "fa-solid fa-vector-square"}, + "DEFAULT": {"color": ["#00bcd4", "#03a9f4"], "icon": "fa-regular fa-file"}, + "PARENT": {"color": ["#424242", "#ffffff"], "icon": "fa-regular fa-folder-open"} } \ No newline at end of file diff --git a/sepal_ui/frontend/styles.py b/sepal_ui/frontend/styles.py index a8bfff93..b0496faa 100644 --- a/sepal_ui/frontend/styles.py +++ b/sepal_ui/frontend/styles.py @@ -153,9 +153,11 @@ class Styles(v.VuetifyTemplate): """ css = (CSS_DIR / "custom.css").read_text() - cdn = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" + cdn = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" + key = "sha512-MV7K8+y+gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK+iQrJ7lzPJQd1w==" template = Unicode( - f'' + f"" + f'' ).tag(sync=True) "Unicode: the trait embeding the maps style" diff --git a/sepal_ui/mapping/aoi_control.py b/sepal_ui/mapping/aoi_control.py index 7b5e6ac1..fc7721e0 100644 --- a/sepal_ui/mapping/aoi_control.py +++ b/sepal_ui/mapping/aoi_control.py @@ -3,7 +3,7 @@ from sepal_ui import sepalwidgets as sw from sepal_ui.mapping.menu_control import MenuControl -from sepal_ui.scripts import utils as su +from sepal_ui.scripts import decorator as sd class AoiControl(MenuControl): @@ -36,7 +36,7 @@ def __init__(self, m, **kwargs): self.aoi_list = sw.ListItemGroup(children=[], v_model="") # create the widget - super().__init__("fas fa-search-location", self.aoi_list, **kwargs) + super().__init__("fa-solid fa-search-location", self.aoi_list, **kwargs) # change a bit the behavior of the control self.menu.open_on_hover = True @@ -70,7 +70,7 @@ def click_btn(self, widget, event, data): return - @su.need_ee + @sd.need_ee def add_aoi(self, name, item): """ Add an AOI to the list and refresh the list displayed. the AOI will be composed of a name and the bounds of the provided item. diff --git a/sepal_ui/mapping/draw_control.py b/sepal_ui/mapping/draw_control.py index e3d1d77f..e030e81a 100644 --- a/sepal_ui/mapping/draw_control.py +++ b/sepal_ui/mapping/draw_control.py @@ -30,7 +30,7 @@ def __init__(self, m, **kwargs): kwargs["circle"] = kwargs.pop("circle", options) kwargs["polygon"] = kwargs.pop("polygon", options) - # save the map in the memeber of the objects + # save the map in the member of the objects self.m = m super().__init__(**kwargs) @@ -41,7 +41,7 @@ def show(self): """ self.clear() - self in self.m.controls or self.m.add_control(self) + self in self.m.controls or self.m.add(self) return @@ -51,7 +51,7 @@ def hide(self): """ self.clear() - self not in self.m.controls or self.m.remove_control(self) + self not in self.m.controls or self.m.remove(self) return diff --git a/sepal_ui/mapping/fullscreen_control.py b/sepal_ui/mapping/fullscreen_control.py index cf961fb9..e1e35988 100644 --- a/sepal_ui/mapping/fullscreen_control.py +++ b/sepal_ui/mapping/fullscreen_control.py @@ -23,7 +23,7 @@ class FullScreenControl(WidgetControl): kwargs (optional): any available arguments from a ipyleaflet WidgetControl """ - ICONS = ["fas fa-expand", "fas fa-compress"] + ICONS = ["fa-solid fa-expand", "fa-solid fa-compress"] "list: The icons that will be used to toggle between expand and compressed mode" METHODS = ["embed", "fullscreen"] diff --git a/sepal_ui/mapping/map_btn.py b/sepal_ui/mapping/map_btn.py index 109f13ee..f3a38927 100644 --- a/sepal_ui/mapping/map_btn.py +++ b/sepal_ui/mapping/map_btn.py @@ -14,13 +14,13 @@ class MapBtn(v.Btn, sw.SepalWidget): The MapBtn is responsive to theme changes. It only accept icon or 3 letters as children as the space is very limited. Args: - content (str): a fas/mdi fully qualified name or a string name. If a string name is used, only the 3 first letters will be displayed. + content (str): a fa-solid/mdi fully qualified name or a string name. If a string name is used, only the 3 first letters will be displayed. """ def __init__(self, content, **kwargs): # create the icon - if content.startswith("mdi-") or content.startswith("fas fa-"): + if content.startswith("mdi-") or content.startswith("fa-solid fa-"): content = sw.Icon(small=True, children=[content]) else: content = content[: min(3, len(content))].upper() diff --git a/sepal_ui/mapping/sepal_map.py b/sepal_ui/mapping/sepal_map.py index 22acef51..2bf6a4e3 100644 --- a/sepal_ui/mapping/sepal_map.py +++ b/sepal_ui/mapping/sepal_map.py @@ -36,6 +36,7 @@ from sepal_ui.mapping.legend_control import LegendControl from sepal_ui.mapping.value_inspector import ValueInspector from sepal_ui.message import ms +from sepal_ui.scripts import decorator as sd from sepal_ui.scripts import utils as su from sepal_ui.scripts.warning import SepalWarning @@ -108,7 +109,7 @@ def __init__( not gee or su.init_ee() # add the basemaps - self.clear_layers() + self.clear() default_basemap = ( "CartoDB.DarkMatter" if v.theme.dark is True else "CartoDB.Positron" ) @@ -116,22 +117,22 @@ def __init__( [self.add_basemap(basemap) for basemap in set(basemaps)] # add the base controls - self.add_control(ipl.ZoomControl(position="topright")) - self.add_control(ipl.LayersControl(position="topright")) - self.add_control(ipl.AttributionControl(position="bottomleft", prefix="SEPAL")) - self.add_control(ipl.ScaleControl(position="bottomleft", imperial=False)) + self.add(ipl.ZoomControl(position="topright")) + self.add(ipl.LayersControl(position="topright")) + self.add(ipl.AttributionControl(position="bottomleft", prefix="SEPAL")) + self.add(ipl.ScaleControl(position="bottomleft", imperial=False)) # specific drawing control self.dc = DrawControl(self) - not dc or self.add_control(self.dc) + not dc or self.add(self.dc) # specific v_inspector self.v_inspector = ValueInspector(self) - not vinspector or self.add_control(self.v_inspector) + not vinspector or self.add(self.v_inspector) # specific statebar self.state = LayerStateControl(self) - not statebar or self.add_control(self.state) + not statebar or self.add(self.state) # create a proxy ID to the element # this id should be unique and will be used by mutators to identify this map @@ -186,7 +187,7 @@ def set_center(self, lon, lat, zoom=None): return - @su.need_ee + @sd.need_ee def zoom_ee_object(self, item, zoom_out=1): """ Get the proper zoom to the given ee geometry. @@ -465,7 +466,7 @@ def add_colorbar( output.clear_output() plt.show() - self.add_control(colormap_ctrl) + self.add(colormap_ctrl) return @@ -763,7 +764,7 @@ def remove_layer(self, key, base=False, none_ok=False): # the error is catched in find_layer if layer is not None: - super().remove_layer(layer) + super().remove(layer) return @@ -814,7 +815,7 @@ def add_layer(self, layer, hover=False): hover_style = default_hover_style if hover else layer.hover_style layer.hover_style = layer.hover_style or hover_style - super().add_layer(layer) + super().add(layer) return @@ -896,7 +897,7 @@ def add_legend( legend_dict, title=title, vertical=vertical, position=position ) - return self.add_control(self.legend) + return self.add(self.legend) # ########################################################################## # ### overwrite geemap calls ### diff --git a/sepal_ui/mapping/value_inspector.py b/sepal_ui/mapping/value_inspector.py index 5cebbd77..733960ea 100644 --- a/sepal_ui/mapping/value_inspector.py +++ b/sepal_ui/mapping/value_inspector.py @@ -15,7 +15,7 @@ from sepal_ui.mapping.layer import EELayer from sepal_ui.mapping.menu_control import MenuControl from sepal_ui.message import ms -from sepal_ui.scripts import utils as su +from sepal_ui.scripts import decorator as sd class ValueInspector(MenuControl): @@ -58,7 +58,7 @@ def __init__(self, m, **kwargs): self.text = sw.CardText(children=[ms.v_inspector.landing]) # create the menu widget - super().__init__("fas fa-crosshairs", self.text, title, **kwargs) + super().__init__("fa-solid fa-crosshairs", self.text, title, **kwargs) # adapt the size self.set_size(min_height=0) @@ -142,7 +142,7 @@ def read_data(self, **kwargs): return - @su.need_ee + @sd.need_ee def _from_eelayer(self, ee_obj, coords): """ extract the values of the ee_object for the considered point diff --git a/sepal_ui/reclassify/reclassify_model.py b/sepal_ui/reclassify/reclassify_model.py index 9d0126f8..1238ddd5 100644 --- a/sepal_ui/reclassify/reclassify_model.py +++ b/sepal_ui/reclassify/reclassify_model.py @@ -12,6 +12,7 @@ from sepal_ui.message import ms from sepal_ui.model import Model +from sepal_ui.scripts import decorator as sd from sepal_ui.scripts import gee from sepal_ui.scripts import utils as su @@ -102,7 +103,7 @@ def __init__( gee=False, dst_dir=Path.home(), aoi_model=None, - folder=None, + folder="", save=True, enforce_aoi=False, **kwargs, @@ -122,7 +123,7 @@ def __init__( su.init_ee() if self.gee: - self.folder = folder or ee.data.getAssetRoots()[0]["id"] + self.folder = str(folder) or ee.data.getAssetRoots()[0]["id"] else: self.folder = None @@ -217,12 +218,12 @@ def get_bands(self): integer or str """ - @su.need_ee + @sd.need_ee def _ee_image(): return ee.Image(self.src_gee).bandNames().getInfo() - @su.need_ee + @sd.need_ee def _ee_vector(): columns = ee.FeatureCollection(self.src_gee).first().getInfo()["properties"] @@ -256,6 +257,9 @@ def _local_vector(): def get_aoi(self): """Validate and get feature collection from aoi_model""" + # by default it's none + aoi = None + # return None if no aoi_model is selected if not self.aoi_model: return @@ -265,8 +269,6 @@ def get_aoi(self): if self.aoi_model.gdf is None: if self.enforce_aoi: raise Exception("You have to select an area of interest before") - else: - aoi = None else: # return the aoi as a vector if self.gee: @@ -289,7 +291,7 @@ def unique(self): if not self.band: raise Exception("You need to provide a band/property to reclassify.") - @su.need_ee + @sd.need_ee def _ee_image(): # reduce the image @@ -309,7 +311,7 @@ def _ee_image(): return values - @su.need_ee + @sd.need_ee def _ee_vector(): collection = ee.FeatureCollection(self.src_gee) @@ -371,7 +373,7 @@ def reclassify(self): if not self.band: raise Exception("You need to provide a band/property to reclassify.") - @su.need_ee + @sd.need_ee def _ee_image(): if not self.src_gee: @@ -438,7 +440,7 @@ def _ee_image(): return self.dst_gee - @su.need_ee + @sd.need_ee def _ee_vector(): if not self.src_gee: diff --git a/sepal_ui/reclassify/reclassify_tile.py b/sepal_ui/reclassify/reclassify_tile.py index 1fcda6e0..e3b7c9a5 100644 --- a/sepal_ui/reclassify/reclassify_tile.py +++ b/sepal_ui/reclassify/reclassify_tile.py @@ -42,7 +42,7 @@ def __init__( dst_class=None, default_class={}, aoi_model=None, - folder=None, + folder="", **kwargs ): diff --git a/sepal_ui/reclassify/reclassify_view.py b/sepal_ui/reclassify/reclassify_view.py index f4d6ca40..e8ba6568 100644 --- a/sepal_ui/reclassify/reclassify_view.py +++ b/sepal_ui/reclassify/reclassify_view.py @@ -6,8 +6,8 @@ import sepal_ui.sepalwidgets as sw from sepal_ui.message import ms +from sepal_ui.scripts import decorator as sd from sepal_ui.scripts import utils as su -from sepal_ui.scripts.utils import loading_button from .parameters import MATRIX_NAMES, NO_VALUE from .reclassify_model import ReclassifyModel @@ -33,8 +33,8 @@ def __init__(self, folder, **kwargs): # create the 3 widgets title = v.CardTitle(children=["Load reclassification matrix"]) self.w_file = sw.FileInput(label="filename", folder=folder) - self.load_btn = sw.Btn("Load") - cancel = sw.Btn("Cancel", outlined=True) + self.load_btn = sw.Btn(msg="Load") + cancel = sw.Btn(msg="Cancel", outlined=True) actions = v.CardActions(children=[cancel, self.load_btn]) # default params @@ -81,8 +81,8 @@ def __init__(self, folder=Path.home(), **kwargs): # create the widgets title = v.CardTitle(children=["Save matrix"]) self.w_file = v.TextField(label="filename", v_model=None) - btn = sw.Btn("Save matrix") - cancel = sw.Btn("Cancel", outlined=True) + btn = sw.Btn(msg="Save matrix") + cancel = sw.Btn(msg="Cancel", outlined=True) actions = v.CardActions(children=[cancel, btn]) self.alert = sw.Alert(children=["Choose a name for the output"]).show() @@ -299,7 +299,7 @@ def _update_matrix_values(self, change): class ReclassifyView(sw.Card): """ Stand-alone Card object allowing the user to reclassify a input file. the input can be of any type (vector or raster) and from any source (local or GEE). - The user need to provide a destination classification file (table) in the following format : 3 headless columns: 'code', 'desc', 'color'. Once all the old class have been attributed to their new class the file can be exported in the source format to local memory or GEE. the output is also savec in memory for further use in the app. It can be used as a tile in a sepal_ui app. The id\_ of the tile is set to "reclassify_tile" + The user need to provide a destination classification file (table) in the following format : 3 headless columns: 'code', 'desc', 'color'. Once all the old class have been attributed to their new class the file can be exported in the source format to local memory or GEE. the output is also savec in memory for further use in the app. It can be used as a tile in a sepal_ui app. The id\\_ of the tile is set to "reclassify_tile" Args: model (ReclassifyModel): the reclassify model to manipulate the @@ -365,7 +365,7 @@ def __init__( default_class={}, aoi_model=None, save=True, - folder=None, + folder="", enforce_aoi=False, **kwargs, ): @@ -464,7 +464,7 @@ def __init__( self.btn_list = [ sw.Btn( - "Custom", + msg="Custom", _metadata={"path": "custom"}, small=True, class_="mr-2", @@ -472,7 +472,7 @@ def __init__( ) ] + [ sw.Btn( - f"use {name}", + msg=f"use {name}", _metadata={"path": path}, small=True, class_="mr-2", @@ -490,18 +490,26 @@ def __init__( self.save_dialog = SaveMatrixDialog(folder=out_path) self.import_dialog = ImportMatrixDialog(folder=out_path) self.get_table = sw.Btn( - ms.rec.rec.input.btn, "far fa-table", color="success", small=True + msg=ms.rec.rec.input.btn, + gliph="fa-solid fa-table", + color="success", + small=True, ) self.import_table = sw.Btn( - "import", - "fas fa-download", + msg="import", + gliph="fa-solid fa-download", color="secondary", small=True, class_="ml-2 mr-2", ) - self.save_table = sw.Btn("save", "fas fa-save", color="secondary", small=True) + self.save_table = sw.Btn( + msg="save", gliph="fa-solid fa-save", color="secondary", small=True + ) self.reclassify_btn = sw.Btn( - ms.rec.rec.btn, "fas fa-chess-board", small=True, disabled=True + msg=ms.rec.rec.btn, + gliph="fa-solid fa-chess-board", + small=True, + disabled=True, ) self.toolbar = v.Toolbar( @@ -554,13 +562,13 @@ def __init__( ] # Decorate functions - self.reclassify = loading_button(self.alert, self.reclassify_btn, debug=True)( - self.reclassify - ) - self.get_reclassify_table = loading_button( + self.reclassify = sd.loading_button( + self.alert, self.reclassify_btn, debug=True + )(self.reclassify) + self.get_reclassify_table = sd.loading_button( self.alert, self.get_table, debug=True )(self.get_reclassify_table) - self.load_matrix_content = loading_button( + self.load_matrix_content = sd.loading_button( self.alert, self.import_table, debug=True )(self.load_matrix_content) @@ -667,7 +675,7 @@ def reclassify(self, widget, event, data): return self - @su.switch("loading", "disabled", on_widgets=["w_code"]) + @sd.switch("loading", "disabled", on_widgets=["w_code"]) def _update_band(self, change): """Update the band possibility to the available bands/properties of the input""" @@ -679,8 +687,8 @@ def _update_band(self, change): return self - @su.switch("disabled", on_widgets=["reclassify_btn"], targets=[False]) - @su.switch("table_created", on_widgets=["model"], targets=[True]) + @sd.switch("disabled", on_widgets=["reclassify_btn"], targets=[False]) + @sd.switch("table_created", on_widgets=["model"], targets=[True]) def get_reclassify_table(self, widget, event, data): """ Display a reclassify table which will lead the user to select diff --git a/sepal_ui/reclassify/table_view.py b/sepal_ui/reclassify/table_view.py index c3f8a35a..63849da1 100644 --- a/sepal_ui/reclassify/table_view.py +++ b/sepal_ui/reclassify/table_view.py @@ -9,6 +9,7 @@ from sepal_ui import sepalwidgets as sw from sepal_ui.message import ms from sepal_ui.reclassify import parameters as param +from sepal_ui.scripts import decorator as sd from sepal_ui.scripts import utils as su __all__ = ["TableView"] @@ -49,19 +50,27 @@ def __init__(self, out_path=Path.home() / "downloads", **kwargs): # create the 4 CRUD btn # and set them in the top slot of the table self.edit_btn = sw.Btn( - ms.rec.table.btn.edit, - icon="fas fa-pencil-alt", + msg=ms.rec.table.btn.edit, + gliph="fa-solid fa-pencil-alt", class_="ml-2 mr-2", color="secondary", small=True, ) self.delete_btn = sw.Btn( - ms.rec.table.btn.delete, icon="fas fa-trash-alt", color="error", small=True + msg=ms.rec.table.btn.delete, + gliph="fa-solid fa-trash-alt", + color="error", + small=True, ) self.add_btn = sw.Btn( - ms.rec.table.btn.add, icon="fas fa-plus", color="success", small=True + msg=ms.rec.table.btn.add, + gliph="fa-solid fa-plus", + color="success", + small=True, + ) + self.save_btn = sw.Btn( + msg=ms.rec.table.btn.save, gliph="fa-regular fa-save", small=True ) - self.save_btn = sw.Btn(ms.rec.table.btn.save, icon="far fa-save", small=True) slot = v.Toolbar( class_="d-flex mb-6", @@ -212,20 +221,19 @@ def __init__(self, table, **kwargs): self.title = v.CardTitle(children=[self.TITLES[0]]) # Action buttons - self.save = sw.Btn(ms.rec.table.edit_dialog.btn.save.name) + self.save = sw.Btn(msg=ms.rec.table.edit_dialog.btn.save.name) save_tool = sw.Tooltip( self.save, ms.rec.table.edit_dialog.btn.save.tooltip, bottom=True ) - self.modify = sw.Btn( - ms.rec.table.edit_dialog.btn.modify.name - ).hide() # by default modify is hidden + self.modify = sw.Btn(msg=ms.rec.table.edit_dialog.btn.modify.name) + self.modify.hide() # by default modify is hidden modify_tool = sw.Tooltip( self.modify, ms.rec.table.edit_dialog.btn.modify.tooltip, bottom=True ) self.cancel = sw.Btn( - ms.rec.table.edit_dialog.btn.cancel.name, outlined=True, class_="ml-2" + msg=ms.rec.table.edit_dialog.btn.cancel.name, outlined=True, class_="ml-2" ) cancel_tool = sw.Tooltip( self.cancel, ms.rec.table.edit_dialog.btn.cancel.tooltip, bottom=True @@ -437,7 +445,7 @@ def __init__(self, table, out_path, **kwargs): v_model=ms.rec.table.save_dialog.placeholder, ) - self.save = sw.Btn(ms.rec.table.save_dialog.btn.save.name) + self.save = sw.Btn(msg=ms.rec.table.save_dialog.btn.save.name) save = sw.Tooltip( self.save, ms.rec.table.save_dialog.btn.save.tooltip, @@ -446,7 +454,7 @@ def __init__(self, table, out_path, **kwargs): ) self.cancel = sw.Btn( - ms.rec.table.save_dialog.btn.cancel.name, outlined=True, class_="ml-2" + msg=ms.rec.table.save_dialog.btn.cancel.name, outlined=True, class_="ml-2" ) cancel = sw.Tooltip( self.cancel, ms.rec.table.save_dialog.btn.cancel.tooltip, bottom=True @@ -540,7 +548,7 @@ def _cancel(self, widget, event, data): class TableView(sw.Card): """ - Stand-alone Card object allowing the user to build custom class table. The user can start from an existing table or start from scratch. It gives the oportunity to change: the value, the class name and the color. It can be used as a tile in a sepal_ui app. The id\_ of the tile is set to "classification_tile" + Stand-alone Card object allowing the user to build custom class table. The user can start from an existing table or start from scratch. It gives the oportunity to change: the value, the class name and the color. It can be used as a tile in a sepal_ui app. The id\\_ of the tile is set to "classification_tile" Args: class_path (str|optional): Folder path containing already existing classes. Default to ~/ @@ -600,8 +608,8 @@ def __init__( folder=self.class_path, ) self.btn = sw.Btn( - ms.rec.table.classif.btn, - icon="far fa-table", + msg=ms.rec.table.classif.btn, + gliph="far fa-table", color="success", outlined=True, ) @@ -634,7 +642,7 @@ def __init__( # Events self.btn.on_event("click", self.get_class_table) - @su.loading_button(debug=True) + @sd.loading_button(debug=True) def get_class_table(self, widget, event, data): """ Display class table widget in view diff --git a/sepal_ui/scripts/decorator.py b/sepal_ui/scripts/decorator.py index fb874e6e..1f898c63 100644 --- a/sepal_ui/scripts/decorator.py +++ b/sepal_ui/scripts/decorator.py @@ -13,7 +13,7 @@ from sepal_ui.scripts.warning import SepalWarning ################################################################################ -# This method is a copy of the ne from utils. It should stay there +# This method is a copy of the one from utils. It should stay there # as long as there is deprecation warning in utils, we cannot import it due to a circular # import. This method should then be removed in v3.0 when sd won't be imported by utils # diff --git a/sepal_ui/scripts/gee.py b/sepal_ui/scripts/gee.py index 1f196191..ca8faf5c 100644 --- a/sepal_ui/scripts/gee.py +++ b/sepal_ui/scripts/gee.py @@ -84,19 +84,20 @@ def is_running(task_descripsion): @su.need_ee -def get_assets(folder=None, asset_list=[]): +def get_assets(folder="", asset_list=[]): """ Get all the assets from the parameter folder. every nested asset will be displayed. Args: - folder (str): the initial GEE folder + folder (str|optional): the initial GEE folder asset_list ([assets]| optional): extra element that you would like to add to the asset list Return: ([asset]): the asset list. each asset is a dict with 3 keys: 'type', 'name' and 'id' """ + # set the folder - folder = folder if folder else ee.data.getAssetRoots()[0]["id"] + folder = str(folder) or ee.data.getAssetRoots()[0]["id"] # loop in the assets for asset in ee.data.listAssets({"parent": folder})["assets"]: @@ -110,20 +111,20 @@ def get_assets(folder=None, asset_list=[]): @su.need_ee -def is_asset(asset_name, folder=None): +def is_asset(asset_name, folder=""): """ Check if the asset already exist in the user asset folder Args: asset_descripsion (str) : the descripsion of the asset - folder (str): the folder of the glad assets + folder (str|optional): the folder of the glad assets Return: (bool): true if already in folder """ # get the folder - folder = folder or ee.data.getAssetRoots()[0]["id"] + folder = str(folder) or ee.data.getAssetRoots()[0]["id"] # get all the assets asset_list = get_assets(folder) diff --git a/sepal_ui/scripts/utils.py b/sepal_ui/scripts/utils.py index 9ba51791..e9e06f6b 100644 --- a/sepal_ui/scripts/utils.py +++ b/sepal_ui/scripts/utils.py @@ -4,7 +4,6 @@ import re import string import warnings -from configparser import ConfigParser from pathlib import Path from urllib.parse import urlparse @@ -287,23 +286,7 @@ def set_config_locale(locale): locale (str): a locale name in IETF BCP 47 (no verifications are performed) """ - config = ConfigParser() - - # read the existing file if available - if config_file.is_file(): - config.read(config_file) - - # set the section if needed - if "sepal-ui" not in config.sections(): - config.add_section("sepal-ui") - - # set the value - config.set("sepal-ui", "locale", locale) - - # save back the file - config.write(config_file.open("w")) - - return + return set_config("locale", locale) @deprecated( @@ -317,23 +300,7 @@ def set_config_theme(theme): theme (str): a theme name (currently supporting "dark" and "light") """ - config = ConfigParser() - - # read the existing file if available - if config_file.is_file(): - config.read(config_file) - - # set the section if needed - if "sepal-ui" not in config.sections(): - config.add_section("sepal-ui") - - # set the value - config.set("sepal-ui", "theme", theme) - - # save back the file - config.write(config_file.open("w")) - - return + return set_config("theme", theme) @versionadded(version="2.7.1") diff --git a/sepal_ui/sepalwidgets/alert.py b/sepal_ui/sepalwidgets/alert.py index 68e3f115..de6d4abb 100644 --- a/sepal_ui/sepalwidgets/alert.py +++ b/sepal_ui/sepalwidgets/alert.py @@ -94,9 +94,10 @@ def update_progress(self, progress, msg="Progress", **tqdm_args): self.show() # cast the progress to float + total = tqdm_args.get("total", 1) progress = float(progress) - if not (0 <= progress <= 1): - raise ValueError(f"progress should be in [0, 1], {progress} given") + if not (0 <= progress <= total): + raise ValueError(f"progress should be in [0, {total}], {progress} given") # Prevent adding multiple times if self.progress_output not in self.children: @@ -107,7 +108,7 @@ def update_progress(self, progress, msg="Progress", **tqdm_args): "bar_format", "{l_bar}{bar}{n_fmt}/{total_fmt}" ) tqdm_args["dynamic_ncols"] = tqdm_args.pop("dynamic_ncols", tqdm_args) - tqdm_args["total"] = tqdm_args.pop("total", 100) + tqdm_args["total"] = tqdm_args.pop("total", 1) tqdm_args["desc"] = tqdm_args.pop("desc", msg) tqdm_args["colour"] = tqdm_args.pop("tqdm_args", getattr(color, self.type)) @@ -120,7 +121,7 @@ def update_progress(self, progress, msg="Progress", **tqdm_args): # Initialize bar self.progress_bar.update(0) - self.progress_bar.update(progress * 100 - self.progress_bar.n) + self.progress_bar.update(progress - self.progress_bar.n) if progress == 1: self.progress_bar.close() diff --git a/sepal_ui/sepalwidgets/app.py b/sepal_ui/sepalwidgets/app.py index 577ecc3b..9dea20ec 100644 --- a/sepal_ui/sepalwidgets/app.py +++ b/sepal_ui/sepalwidgets/app.py @@ -54,7 +54,9 @@ def __init__(self, title="SEPAL module", translator=None, **kwargs): self.toggle_button = v.Btn( icon=True, - children=[v.Icon(class_="white--text", children=["fas fa-ellipsis-v"])], + children=[ + v.Icon(class_="white--text", children=["fa-solid fa-ellipsis-v"]) + ], ) self.title = v.ToolbarTitle(children=[title]) @@ -132,7 +134,7 @@ def __init__( # set the resizetrigger self.rt = rt - icon = icon if icon else "far fa-folder" + icon = icon if icon else "fa-regular fa-folder" children = [ v.ListItemAction(children=[v.Icon(class_="white--text", children=[icon])]), @@ -160,7 +162,9 @@ def __init__( # cannot be set as a class member because it will be shared with all # the other draweritems. self.alert_badge = v.ListItemAction( - children=[v.Icon(children=["fas fa-circle"], x_small=True, color="red")] + children=[ + v.Icon(children=["fa-solid fa-circle"], x_small=True, color="red") + ] ) if model: @@ -255,17 +259,17 @@ def __init__(self, items=[], code=None, wiki=None, issue=None, **kwargs): code_link = [] if code: item_code = DrawerItem( - ms.widgets.navdrawer.code, icon="far fa-file-code", href=code + ms.widgets.navdrawer.code, icon="fa-regular fa-file-code", href=code ) code_link.append(item_code) if wiki: item_wiki = DrawerItem( - ms.widgets.navdrawer.wiki, icon="fas fa-book-open", href=wiki + ms.widgets.navdrawer.wiki, icon="fa-solid fa-book-open", href=wiki ) code_link.append(item_wiki) if issue: item_bug = DrawerItem( - ms.widgets.navdrawer.bug, icon="fas fa-bug", href=issue + ms.widgets.navdrawer.bug, icon="fa-solid fa-bug", href=issue ) code_link.append(item_bug) @@ -699,7 +703,7 @@ class ThemeSelect(v.Btn, SepalWidget): kwargs (dict, optional): any arguments for a Btn object, children and v_model will be override """ - THEME_ICONS = {"dark": "fas fa-moon", "light": "fas fa-sun"} + THEME_ICONS = {"dark": "fa-solid fa-moon", "light": "fa-solid fa-sun"} "dict: the dictionnry of icons to use for each theme (used as keys)" theme = "dark" diff --git a/sepal_ui/sepalwidgets/btn.py b/sepal_ui/sepalwidgets/btn.py index c6437d86..b0fae0b4 100644 --- a/sepal_ui/sepalwidgets/btn.py +++ b/sepal_ui/sepalwidgets/btn.py @@ -1,6 +1,9 @@ +import warnings from pathlib import Path import ipyvuetify as v +from deprecated.sphinx import deprecated +from traitlets import Unicode, observe from sepal_ui.scripts import utils as su from sepal_ui.sepalwidgets.sepalwidget import SepalWidget @@ -14,27 +17,87 @@ class Btn(v.Btn, SepalWidget): the color will be defaulted to 'primary' and can be changed afterward according to your need Args: + msg (str, optional): the text to display in the btn + gliph (str, optional): the full name of any mdi/fa icon text (str, optional): the text to display in the btn icon (str, optional): the full name of any mdi/fa icon kwargs (dict, optional): any parameters from v.Btn. if set, 'children' will be overwritten. + + .. deprecated:: 2.13 + ``text`` and ``icon`` will be replaced by ``msg`` and ``gliph`` to avoid duplicating ipyvuetify trait. + + .. deprecated:: 2.14 + Btn is not using a default ``msg`` anymor`. """ v_icon = None "v.Icon: the icon in the btn" - def __init__(self, text="Click", icon="", **kwargs): + gliph = Unicode("").tag(sync=True) + "traitlet.Unicode: the name of the icon" + + msg = Unicode("").tag(sync=True) + "traitlet.Unicode: the text of the btn" + + def __init__(self, msg="", gliph="", **kwargs): + + # deprecation in 2.13 of text and icon + # as they already exist in the ipyvuetify Btn traits (as booleans) + if "text" in kwargs: + if isinstance(kwargs["text"], str): + msg = kwargs.pop("text") + warnings.warn( + '"text" is deprecated, please use "msg" instead', DeprecationWarning + ) + if "icon" in kwargs: + if isinstance(kwargs["icon"], str): + gliph = kwargs.pop("icon") + warnings.warn( + '"icon" is deprecated, please use "gliph" instead', + DeprecationWarning, + ) # create the default v_icon - self.v_icon = v.Icon(left=True, children=[""]) - self.set_icon(icon) + self.v_icon = v.Icon(children=[""]) # set the default parameters kwargs["color"] = kwargs.pop("color", "primary") - kwargs["children"] = [self.v_icon, text] + kwargs["children"] = [self.v_icon, self.msg] # call the constructor super().__init__(**kwargs) + self.gliph = gliph + self.msg = msg + + @observe("gliph") + def _set_gliph(self, change): + """ + Set a new icon. If the icon is set to "", then it's hidden + """ + new_gliph = change["new"] + self.v_icon.children = [new_gliph] + + # hide the component to avoid the right padding + if not new_gliph: + su.hide_component(self.v_icon) + else: + su.show_component(self.v_icon) + + return self + + @observe("msg") + def _set_text(self, change): + """ + Set the text of the btn + """ + + self.v_icon.left = bool(change["new"]) + self.children = [self.v_icon, change["new"]] + + return self + + @deprecated(version="2.14", reason="Replace by the private _set_gliph") def set_icon(self, icon=""): """ set a new icon. If the icon is set to "", then it's hidden. @@ -45,13 +108,7 @@ def set_icon(self, icon=""): Return: self """ - self.v_icon.children = [icon] - - if not icon: - su.hide_component(self.v_icon) - else: - su.show_component(self.v_icon) - + self.gliph = icon return self def toggle_loading(self): @@ -82,7 +139,7 @@ class DownloadBtn(v.Btn, SepalWidget): def __init__(self, text, path="#", **kwargs): # create a download icon - v_icon = v.Icon(left=True, children=["fas fa-download"]) + v_icon = v.Icon(left=True, children=["fa-solid fa-download"]) # set default parameters kwargs["class_"] = kwargs.pop("class_", "ma-2") diff --git a/sepal_ui/sepalwidgets/inputs.py b/sepal_ui/sepalwidgets/inputs.py index 5a9507ad..0c9e7790 100644 --- a/sepal_ui/sepalwidgets/inputs.py +++ b/sepal_ui/sepalwidgets/inputs.py @@ -256,18 +256,25 @@ def __init__( "name": "activator", "variable": "x", "children": Btn( - icon="fas fa-search", v_model=False, v_on="x.on", text=label + gliph="fa-solid fa-search", + v_model=False, + v_on="x.on", + msg=label, ), } ], ) self.reload = v.Btn( - icon=True, color="primary", children=[v.Icon(children=["fas fa-sync-alt"])] + icon=True, + color="primary", + children=[v.Icon(children=["fa-solid fa-sync-alt"])], ) self.clear = v.Btn( - icon=True, color="primary", children=[v.Icon(children=["fas fa-times"])] + icon=True, + color="primary", + children=[v.Icon(children=["fa-solid fa-times"])], ) if not clearable: su.hide_component(self.clear) @@ -666,7 +673,7 @@ class AssetSelect(v.Combobox, SepalWidget): def __init__( self, label=ms.widgets.asset_select.label, - folder=None, + folder="", types=["IMAGE", "TABLE"], default_asset=[], **kwargs, @@ -675,7 +682,7 @@ def __init__( self.asset_info = None # if folder is not set use the root one - self.folder = folder if folder else ee.data.getAssetRoots()[0]["id"] + self.folder = str(folder) or ee.data.getAssetRoots()[0]["id"] self.types = types # load the default assets diff --git a/sepal_ui/sepalwidgets/widget.py b/sepal_ui/sepalwidgets/widget.py index 4f457e9a..e252fee3 100644 --- a/sepal_ui/sepalwidgets/widget.py +++ b/sepal_ui/sepalwidgets/widget.py @@ -66,7 +66,7 @@ def __init__(self, **kwargs): kwargs["outlined"] = kwargs.pop("outlined", True) kwargs["label"] = kwargs.pop("label", "Copy To clipboard") kwargs["readonly"] = kwargs.pop("readonly", True) - kwargs["append_icon"] = kwargs.pop("append_icon", "fas fa-clipboard") + kwargs["append_icon"] = kwargs.pop("append_icon", "fa-solid fa-clipboard") kwargs["v_model"] = kwargs.pop("v_model", None) kwargs["class_"] = kwargs.pop("class_", "ma-5") @@ -102,7 +102,7 @@ def __init__(self, **kwargs): def _clip(self, widget, event, data): self.send({"method": "clip", "args": [self.tf.v_model]}) - self.tf.append_icon = "fas fa-clipboard-check" + self.tf.append_icon = "fa-solid fa-clipboard-check" return @@ -142,7 +142,9 @@ def __init__(self, model=None, model_trait=None, states=None, **kwargs): # Get the first value (states first key) to use as default one init_value = self.states[next(iter(self.states))] - self.icon = v.Icon(children=["fas fa-circle"], color=init_value[1], small=True) + self.icon = v.Icon( + children=["fa-solid fa-circle"], color=init_value[1], small=True + ) super().__init__(self.icon, init_value[0], **kwargs) diff --git a/sepal_ui/templates/map_app/component/message/en/app.json b/sepal_ui/templates/map_app/component/message/en/app.json index cd3a73ae..13ae2f87 100644 --- a/sepal_ui/templates/map_app/component/message/en/app.json +++ b/sepal_ui/templates/map_app/component/message/en/app.json @@ -1,6 +1,6 @@ { "app": { - "title": "Panel application", + "title": "Map application", "footer": "The sky is the limit \u00A9 {}", "banner": "This is a automatically generated application. Remove this banner once your application is ready.", "link": { diff --git a/sepal_ui/templates/map_app/component/tile/map_tile.py b/sepal_ui/templates/map_app/component/tile/map_tile.py index 70fde8d8..11eb339b 100644 --- a/sepal_ui/templates/map_app/component/tile/map_tile.py +++ b/sepal_ui/templates/map_app/component/tile/map_tile.py @@ -1,4 +1,5 @@ from ipyleaflet import WidgetControl + from sepal_ui import mapping as sm from sepal_ui import sepalwidgets as sw @@ -20,26 +21,26 @@ def __init__(self): def set_code(self, link): "add the code link btn to the map" - btn = sm.MapBtn("fas fa-code", href=link, target="_blank") + btn = sm.MapBtn("fa-solid fa-code", href=link, target="_blank") control = WidgetControl(widget=btn, position="bottomleft") - self.m.add_control(control) + self.m.add(control) return def set_wiki(self, link): "add the wiki link btn to the map" - btn = sm.MapBtn("fas fa-book-open", href=link, target="_blank") + btn = sm.MapBtn("fa-solid fa-book-open", href=link, target="_blank") control = WidgetControl(widget=btn, position="bottomleft") - self.m.add_control(control) + self.m.add(control) return def set_issue(self, link): "add the code link btn to the map" - btn = sm.MapBtn("fas fa-bug", href=link, target="_blank") + btn = sm.MapBtn("fa-solid fa-bug", href=link, target="_blank") control = WidgetControl(widget=btn, position="bottomleft") - self.m.add_control(control) + self.m.add(control) return diff --git a/sepal_ui/templates/panel_app/ui.ipynb b/sepal_ui/templates/panel_app/ui.ipynb index e4c89001..745b9650 100644 --- a/sepal_ui/templates/panel_app/ui.ipynb +++ b/sepal_ui/templates/panel_app/ui.ipynb @@ -55,7 +55,7 @@ "# create a drawer for each group of tile\n", "# fmt: off\n", "items = [\n", - " sw.DrawerItem(cm.app.drawer_item.about, \"fas fa-question-circle\", card=\"about_tile\"),\n", + " sw.DrawerItem(cm.app.drawer_item.about, \"fa-solid fa-question-circle\", card=\"about_tile\"),\n", "]\n", "# fmt: on\n", "\n", diff --git a/setup.py b/setup.py index 52fba5d5..54b8624c 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ def get_templates(): "cryptography", "python-box", "xyzservices", - "planet, + "planet==2.0a6", "pyyaml", "dask", "tqdm", @@ -80,6 +80,11 @@ def get_templates(): "test": [ "coverage", "pytest", + "pytest-sugar", + "pytest-icdiff", + "pytest-instafail", + "pytest-deadfixtures", + "pytest-cov", "nbmake ", ], "doc": [ @@ -131,6 +136,7 @@ def get_templates(): "module_theme = sepal_ui.bin.module_theme:main", "module_venv = sepal_ui.bin.module_venv:main", "activate_venv = sepal_ui.bin.activate_venv:main", + "ee_token = sepal_ui.bin.ee_token:main", ] }, "classifiers": [ diff --git a/tests/conftest.py b/tests/conftest.py index 96bca223..1877f744 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,28 +1,38 @@ -import os +import uuid +from itertools import product from pathlib import Path +import ee +import geopandas as gpd import pytest +from shapely import geometry as sg import sepal_ui.sepalwidgets as sw +from sepal_ui.scripts import gee +from sepal_ui.scripts import utils as su - -@pytest.fixture(scope="session") -def gee_ready(): - """return if exporting is possible with the current GEE authentification method""" - - return "EE_DECRYPT_KEY" not in os.environ +# try to init earthengine. if it does not work the non existing credentials will +# be used to skpip +try: + su.init_ee() +except Exception: + pass @pytest.fixture(scope="session") def root_dir(): - """path to the root dir of the librairy""" + """ + Path to the root dir of the librairy + """ return Path(__file__).parents[1].absolute() @pytest.fixture(scope="session") def tmp_dir(): - """Creates a temporary local directory""" + """ + Creates a temporary local directory to store data + """ tmp_dir = Path.home() / "tmp" / "sepal_ui_tests" tmp_dir.mkdir(exist_ok=True, parents=True) @@ -31,45 +41,96 @@ def tmp_dir(): @pytest.fixture(scope="session") -def gee_dir(): - """the test dir allowed with the service account credentials""" - - return "projects/earthengine-legacy/assets/users/bornToBeAlive/sepal_ui_test" - - -@pytest.fixture(scope="session") -def asset_france(gee_dir): - """return the france asset available in our test account""" - - return f"{gee_dir}/france" - - -@pytest.fixture(scope="session") -def asset_italy(gee_dir): - """return the italy asset available in our test account""" - - return f"{gee_dir}/italy" - - -@pytest.fixture(scope="session") -def asset_table_aoi(gee_dir): - """return the aoi for the reclassify tests available in our test account""" - - return f"{gee_dir}/reclassify_table_aoi" - - -@pytest.fixture(scope="session") -def asset_image_aoi(gee_dir): - """return the aoi for the reclassify tests available in our test account""" +def _hash(): + """ + Create a hash for each test instance + """ - return f"{gee_dir}/reclassify_image_aoi" + return uuid.uuid4().hex @pytest.fixture(scope="session") -def no_name(): - """return a no-name tuple""" - - return ("no_name", "#000000") +def gee_dir(_hash): + """ + Create a test dir based on earthengine initialization + populate it with fake super small assets: + + sepal-ui-/ + ├── subfolder/ + │ └── subfolder_feature_collection + ├── feature_collection + └── image + + remove everything on teardown + """ + if not ee.data._credentials: + pytest.skip("Eathengine is not connected") + + # create a test folder with a hash name + root = ee.data.getAssetRoots()[0]["id"] + gee_dir = Path(root) / f"sepal-ui-{_hash}" + ee.data.createAsset({"type": "FOLDER"}, str(gee_dir)) + + # create a subfolder + subfolder = gee_dir / "subfolder" + ee.data.createAsset({"type": "FOLDER"}, str(subfolder)) + + # create test material + centers = [sg.Point(i, j) for i, j in product([-50, 50], repeat=2)] + data = list(range(len(centers))) + gdf = gpd.GeoDataFrame({"data": data, "geometry": centers}, crs=3857).to_crs(4326) + ee_gdf = ee.FeatureCollection(gdf.__geo_interface__) + + image = ee.Image.random().multiply(4).byte() + + lon = ee.Image.pixelLonLat().select("longitude") + lat = ee.Image.pixelLonLat().select("latitude") + image = ( + ee.Image(1) + .where(lon.gt(0).And(lat.gt(0)), 2) + .where(lon.lte(0).And(lat.lte(0)), 3) + .where(lon.gt(0).And(lat.lte(0)), 4) + ) + ee_buffer = ee.Geometry.Point(0, 0).buffer(200).bounds() + image = image.clipToBoundsAndScale(ee_buffer, scale=30) + + # exports It should take less than 2 minutes unless there are concurent tasks + fc = "feature_collection" + ee.batch.Export.table.toAsset( + collection=ee_gdf, description=f"{fc}_{_hash}", assetId=str(gee_dir / fc) + ).start() + + subfolder_fc = "subfolder_feature_collection" + ee.batch.Export.table.toAsset( + collection=ee_gdf, + description=f"{subfolder_fc}_{_hash}", + assetId=str(subfolder / subfolder_fc), + ).start() + + rand_image = "image" + ee.batch.Export.image.toAsset( + image=image, + description=f"{rand_image}_{_hash}", + assetId=str(gee_dir / rand_image), + region=ee_buffer, + ).start() + + # wait for completion of the exportation tasks before leaving this method + # image should be the longest + gee.wait_for_completion(f"{fc}_{_hash}") + gee.wait_for_completion(f"{subfolder_fc}_{_hash}") + gee.wait_for_completion(f"{rand_image}_{_hash}") + + yield gee_dir + + # flush the directory and it's content + ee.data.deleteAsset(str(subfolder / subfolder_fc)) + ee.data.deleteAsset(str(subfolder)) + ee.data.deleteAsset(str(gee_dir / fc)) + ee.data.deleteAsset(str(gee_dir / rand_image)) + ee.data.deleteAsset(str(gee_dir)) + + return @pytest.fixture @@ -77,31 +138,3 @@ def alert(): """return a dummy alert that can be used everywhere to display informations""" return sw.Alert() - - -@pytest.fixture(scope="session") -def readme(root_dir): - """return the readme file path""" - - return root_dir / "README.rst" - - -@pytest.fixture(scope="session") -def asset_description(): - """return a test asset name""" - - return "test_travis" - - -@pytest.fixture(scope="session") -def asset_id(asset_description): - """return a test asset id""" - - return f"users/bornToBeAlive/sepal_ui_test/{asset_description}" - - -@pytest.fixture(scope="session") -def asset_image_viz(): - """return a test asset id""" - - return "users/bornToBeAlive/sepal_ui_test/imageViZExample" diff --git a/tests/test_Alert.py b/tests/test_Alert.py index fd99907d..52c6931c 100644 --- a/tests/test_Alert.py +++ b/tests/test_Alert.py @@ -2,6 +2,7 @@ from sepal_ui import sepalwidgets as sw from sepal_ui.frontend.styles import TYPES +from sepal_ui.scripts.warning import SepalWarning class TestAlert: @@ -18,14 +19,14 @@ def test_init(self): assert alert.type == type_ # wrong type - alert = sw.Alert("random") - assert alert.type == "info" + with pytest.warns(SepalWarning): + alert = sw.Alert("random") + assert alert.type == "info" return - def test_add_msg(self): + def test_add_msg(self, alert): - alert = sw.Alert() msg = "toto" # single msg @@ -41,15 +42,15 @@ def test_add_msg(self): assert alert.children[0].children[0] == msg # single msg with rdm type - alert.add_msg(msg, "random") - assert alert.type == "info" - assert alert.children[0].children[0] == msg + with pytest.warns(SepalWarning): + alert.add_msg(msg, "random") + assert alert.type == "info" + assert alert.children[0].children[0] == msg return - def test_add_live_msg(self): + def test_add_live_msg(self, alert): - alert = sw.Alert() msg = "toto" # single msg @@ -65,9 +66,10 @@ def test_add_live_msg(self): assert alert.children[1].children[0] == msg # single msg with rdm type - alert.add_live_msg(msg, "random") - assert alert.type == "info" - assert alert.children[1].children[0] == msg + with pytest.warns(SepalWarning): + alert.add_live_msg(msg, "random") + assert alert.type == "info" + assert alert.children[1].children[0] == msg return @@ -107,32 +109,9 @@ def test_append_msg(self): alert.type = "success" assert alert.children[1].type_ == "success" - def test_check_input(self): - - alert = sw.Alert() - - with pytest.raises(ValueError, match="The value has not been initialized"): - alert.check_input(None) - - with pytest.raises(ValueError, match="toto"): - alert.check_input(None, "toto") - - res = alert.check_input(1) - assert res is True + def test_reset(self, alert): - # test lists - res = alert.check_input([range(2)]) - assert res is True - - # test empty list - with pytest.raises(ValueError): - alert.check_input([]) - - return - - def test_reset(self): - - alert = sw.Alert().add_msg("toto").reset() + alert.add_msg("toto").reset() assert alert.viz is False assert len(alert.children) == 1 @@ -170,16 +149,22 @@ def test_rmv_last_msg(self): return - def test_update_progress(self): - - # create an alert - alert = sw.Alert() + def test_update_progress(self, alert): # test a random update alert.update_progress(0.5) - assert alert.progress_bar.n == 50 + assert alert.progress_bar.n == 0.5 assert alert.viz is True # show that a value > 1 raise an error with pytest.raises(ValueError): + alert.reset() alert.update_progress(1.5) + + # check that if total is set value can be more than 1 + alert.reset() + alert.update_progress(50, total=100) + assert alert.progress_bar.n == 50 + assert alert.viz is True + + return diff --git a/tests/test_AoiControl.py b/tests/test_AoiControl.py index 0a4c3034..873357fc 100644 --- a/tests/test_AoiControl.py +++ b/tests/test_AoiControl.py @@ -4,7 +4,6 @@ from shapely import geometry as sg from sepal_ui import mapping as sm -from sepal_ui.scripts import utils as su class TestAoiControl: @@ -13,40 +12,40 @@ def test_init(self): # check that the map start with no info m = sm.SepalMap() control = sm.AoiControl(m) - m.add_control(control) + m.add(control) assert isinstance(control, sm.AoiControl) assert control in m.controls return - def test_add_aoi(self, point, ee_point): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_add_aoi_ee(self, ee_points, aoi_control): - m = sm.SepalMap() - aoi_control = sm.AoiControl(m) + # test with an ee point + aoi_control.add_aoi("test1", ee_points[1]) + assert aoi_control.aoi_bounds["test1"] == (20, 30, 20, 30) + assert aoi_control.aoi_list.children[0].value == (20, 30, 20, 30) + + return + + def test_add_aoi(self, points, aoi_control): # test ith a shapely geometry - aoi_control.add_aoi("test1", point) + aoi_control.add_aoi("test1", points[0]) assert aoi_control.aoi_bounds["test1"] == (10, 20, 10, 20) assert aoi_control.aoi_list.children[0].value == (10, 20, 10, 20) - # test with an ee point - aoi_control.add_aoi("test1", ee_point) - assert aoi_control.aoi_bounds["test1"] == (20, 30, 20, 30) - assert aoi_control.aoi_list.children[0].value == (20, 30, 20, 30) - # test with something else with pytest.raises(ValueError): aoi_control.add_aoi("test3", {}) return - def test_remove_aoi(self, point): + def test_remove_aoi(self, points, aoi_control): # add a point - m = sm.SepalMap() - aoi_control = sm.AoiControl(m) - aoi_control.add_aoi("test1", point) + aoi_control.add_aoi("test1", points[0]) # test that I can remove it aoi_control.remove_aoi("test1") @@ -59,13 +58,12 @@ def test_remove_aoi(self, point): return - def test_click_btn(self, point, ee_point): + def test_click_btn(self, points, aoi_control): # create the map - m = sm.SepalMap() + m = aoi_control.m m.center = [43, 25] # anywhere m.zoom = 10 - aoi_control = sm.AoiControl(m) # zoom on it with nothing aoi_control.click_btn(None, None, None) @@ -74,8 +72,8 @@ def test_click_btn(self, point, ee_point): assert m.zoom == 2.0 # zoom on it with 2 points - aoi_control.add_aoi("test1", point) - aoi_control.add_aoi("test2", ee_point) + aoi_control.add_aoi("test1", points[0]) + aoi_control.add_aoi("test2", points[1]) aoi_control.click_btn(None, None, None) assert m.center == [25.0, 15.0] @@ -83,30 +81,35 @@ def test_click_btn(self, point, ee_point): return - def test_zoom(self, point): - - # add a point - m = sm.SepalMap() - aoi_control = sm.AoiControl(m) - aoi_control.add_aoi("test1", point) + def test_zoom(self, points, aoi_control): - # zoom on it - aoi_control.zoom(Box({"value": point.bounds}), None, None) + # add a point and zoom + aoi_control.add_aoi("test1", points[0]) + aoi_control.zoom(Box({"value": points[0].bounds}), None, None) - assert m.center == [20.0, 10.0] - assert m.zoom == 1090.0 + assert aoi_control.m.center == [20.0, 10.0] + assert aoi_control.m.zoom == 1090.0 return - @pytest.fixture - def point(self): - """return a point""" + @pytest.fixture(scope="class") + def points(self): + """return a tuple of points""" + + return (sg.Point(10, 20), sg.Point(20, 30)) + + @pytest.fixture(scope="class") + def ee_points(self): + """return a tuple of ee_point""" - return sg.Point(10, 20) + return (ee.Geometry.Point(10, 20), ee.Geometry.Point(20, 30)) - @su.need_ee @pytest.fixture - def ee_point(self): - """return a ee_point""" + def aoi_control(self): + """an aoi_control and add it to a map""" + + m = sm.SepalMap() + aoi_control = sm.AoiControl(m) + m.add(aoi_control) - return ee.Geometry.Point(20, 30) + return aoi_control diff --git a/tests/test_AoiModel.py b/tests/test_AoiModel.py index 1ed413ea..40400441 100644 --- a/tests/test_AoiModel.py +++ b/tests/test_AoiModel.py @@ -8,7 +8,27 @@ class TestAoiModel: - def test_init(self, alert, gee_dir, asset_italy, fake_vector): + def test_init_no_ee(self, alert, fake_vector): + + # default init + aoi_model = aoi.AoiModel(alert, gee=False) + assert isinstance(aoi_model, aoi.AoiModel) + assert aoi_model.ee is False + + # with a default vector + aoi_model = aoi.AoiModel(alert, vector=fake_vector, gee=False) + assert aoi_model.name == "gadm36_VAT_0" + + # test with a non ee admin + admin = "VAT" # GADM Vatican city + aoi_model = aoi.AoiModel(alert, gee=False, admin=admin) + + assert aoi_model.name == "VAT" + + return + + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_init_ee(alert, gee_dir): # default init aoi_model = aoi.AoiModel(alert, folder=gee_dir) @@ -16,50 +36,40 @@ def test_init(self, alert, gee_dir, asset_italy, fake_vector): assert aoi_model.ee is True # with default assetId - aoi_model = aoi.AoiModel(alert, asset=asset_italy, folder=gee_dir) + asset_id = str(gee_dir / "feature_collection") + aoi_model = aoi.AoiModel(alert, asset=asset_id, folder=gee_dir) - assert aoi_model.asset_name["pathname"] == asset_italy - assert aoi_model.default_asset["pathname"] == asset_italy + assert aoi_model.asset_name["pathname"] == asset_id + assert aoi_model.default_asset["pathname"] == asset_id assert all(aoi_model.gdf) is not None assert aoi_model.feature_collection is not None - assert aoi_model.name == "italy" + assert aoi_model.name == "feature_collection" - # chack that wrongly defined asset_name raise errors + # check that wrongly defined asset_name raise errors with pytest.raises(Exception): aoi_model = aoi.AoiModel(alert, folder=gee_dir) aoi_model._from_asset({"pathname": None}) with pytest.raises(Exception): aoi_model = aoi.AoiModel(alert, folder=gee_dir) - aoi_model._from_asset( - {"pathname": asset_italy, "column": "ADM0_CODE", "value": None} - ) + asset = {"pathname": asset_id, "column": "data", "value": None} + aoi_model._from_asset(asset) - # it should be the same with a different name - aoi_model = aoi.AoiModel(alert, folder=gee_dir) - aoi_model._from_asset( - {"pathname": asset_italy, "column": "ADM0_CODE", "value": 122} - ) - assert aoi_model.name == "italy_ADM0_CODE_122" + # it should be the same with a different name + aoi_model = aoi.AoiModel(alert, folder=gee_dir) + asset = {"pathname": asset_id, "column": "data", "value": 0} + aoi_model._from_asset(asset) + assert aoi_model.name == "feature_collection_data_0" # with a default admin - admin = 85 # GAUL France + admin = 110 # GAUL Vatican city aoi_model = aoi.AoiModel(alert, admin=admin, folder=gee_dir) - assert aoi_model.name == "FRA" - - # with a default vector - aoi_model = aoi.AoiModel(alert, vector=fake_vector, gee=False) - assert aoi_model.name == "gadm36_VAT_0" - - # test with a non ee definition - admin = "FRA" # GADM France - aoi_model = aoi.AoiModel(alert, gee=False, admin=admin) - - assert aoi_model.name == "FRA" + assert aoi_model.name == "VAT" return - def test_get_columns(self, aoi_model_france): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_get_columns(self, test_model, test_columns): # test that before any data is set the method raise an error with pytest.raises(Exception): @@ -67,23 +77,13 @@ def test_get_columns(self, aoi_model_france): aoi_model.get_columns() # test data - test_data = [ - "ADM0_CODE", - "ADM0_NAME", - "DISP_AREA", - "EXP0_YEAR", - "STATUS", - "STR0_YEAR", - "Shape_Leng", - ] - - res = aoi_model_france.get_columns() - - assert res == test_data + res = test_model.get_columns() + assert res == test_columns return - def test_get_fields(self, aoi_model_france): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_get_fields(self, test_model): # test that before any data is set the method raise an error with pytest.raises(Exception): @@ -92,105 +92,98 @@ def test_get_fields(self, aoi_model_france): # init column = "ADM0_CODE" - - res = aoi_model_france.get_fields(column) - - assert res == [85] + res = test_model.get_fields(column) + assert res == [110] return - def test_get_selected(self, aoi_model_france, asset_france): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_get_selected(self, test_model): # test that before any data is set the method raise an error with pytest.raises(Exception): aoi_model = aoi.AoiModel() aoi_model.get_fields("toto", "toto") - # init - ee_france = ee.FeatureCollection(asset_france) - - # select the geometry associated with france (all of it) - column = "ADM0_CODE" - field = 85 - - feature = aoi_model_france.get_selected(column, field) + # select the vatican feature in GAUL 2015 + ee_vat = ee.FeatureCollection("FAO/GAUL/2015/level0").filter( + ee.Filter.eq("ADM0_CODE", 110) + ) - feature_geom = feature.geometry().getInfo() - france_geom = ee_france.geometry().getInfo() + # select the geometry associated with Vatican city (all of it) + column, field = ("ADM0_CODE", 110) + feature = test_model.get_selected(column, field) - assert feature_geom == france_geom + # assert they are the same + dif = feature.geometry().difference(ee_vat.geometry()).coordinates().length() + assert dif.getInfo() == 0 return - def test_clear_attributes( - self, alert, gee_dir, aoi_model_outputs, aoi_model_traits - ): - - aoi_model = aoi.AoiModel(alert, folder=gee_dir) + def test_clear_attributes(self, alert, aoi_model_outputs, aoi_model_traits): - dum = "dum" + aoi_model = aoi.AoiModel(alert, gee=False) # insert dum parameter everywhere + dum = "dum" [setattr(aoi_model, trait, dum) for trait in aoi_model_traits] [setattr(aoi_model, out, dum) for out in aoi_model_outputs] - # clear them + # clear all the parameters aoi_model.clear_attributes() - assert all([getattr(aoi_model, trait) is None for trait in aoi_model_traits]) - assert all([getattr(aoi_model, out) is None for out in aoi_model_outputs]) + # create a function for readability + def is_none(member): + return getattr(aoi_model, member) is None + + assert all([is_none(trait) for trait in aoi_model_traits]) + assert all([is_none(out) for out in aoi_model_outputs]) # check that default are saved - aoi_model = aoi.AoiModel(alert, admin=85, folder=gee_dir) # GAUL for France + aoi_model = aoi.AoiModel(alert, admin="VAT", gee=False) # GADM for Vatican - # insert dummy args + # insert dummy parameter [setattr(aoi_model, trait, dum) for trait in aoi_model_traits] [setattr(aoi_model, out, dum) for out in aoi_model_outputs] # clear aoi_model.clear_attributes() - # assert that it's still france - assert aoi_model.name == "FRA" + # assert that it's still Vatican + assert aoi_model.name == "VAT" return - def test_total_bounds(self, aoi_model_france): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_total_bounds(self, test_model, test_bounds): - # test data - expected_bounds = ( - -5.142230921252722, - 41.33878298628808, - 9.561552263332496, - 51.09281241936492, - ) + bounds = test_model.total_bounds() + assert bounds == test_bounds - bounds = aoi_model_france.total_bounds() + return - assert bounds == expected_bounds + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_clear_output(self, test_model, aoi_model_outputs): - return + # create functions for readability + def is_not_none(member): + return getattr(test_model, member) is not None - def test_clear_output(self, aoi_model_france, aoi_model_outputs): + def is_none(member): + return getattr(test_model, member) is None # test that the data are not all empty - assert any( - [getattr(aoi_model_france, out) is not None for out in aoi_model_outputs] - ) + assert any([is_not_none(out) for out in aoi_model_outputs]) # clear the aoi outputs - aoi_model_france.clear_output() - assert all( - [getattr(aoi_model_france, out) is None for out in aoi_model_outputs] - ) + test_model.clear_output() + assert all([is_none(out) for out in aoi_model_outputs]) return - def test_set_object( - self, alert, gee_dir, fake_vector, asset_france, fake_points, square - ): + def test_set_object(self, alert): - aoi_model = aoi.AoiModel(alert, folder=gee_dir) + aoi_model = aoi.AoiModel(alert, gee=False) # test that no method returns an error with pytest.raises(Exception): @@ -207,11 +200,12 @@ def test_from_admin(self, alert, gee_dir): aoi_model._from_admin(0) # test france - aoi_model._from_admin(85) - assert aoi_model.name == "FRA" + aoi_model._from_admin(110) + assert aoi_model.name == "VAT" return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_from_point(self, alert, fake_points, gee_dir): aoi_model = aoi.AoiModel(alert, folder=gee_dir, gee=False) @@ -238,6 +232,7 @@ def test_from_point(self, alert, fake_points, gee_dir): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_from_vector(self, alert, gee_dir, fake_vector): aoi_model = aoi.AoiModel(alert, folder=gee_dir, gee=False) @@ -263,6 +258,7 @@ def test_from_vector(self, alert, gee_dir, fake_vector): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_from_geo_json(self, alert, gee_dir, square): aoi_model = aoi.AoiModel(alert, folder=gee_dir, gee=False) @@ -278,32 +274,36 @@ def test_from_geo_json(self, alert, gee_dir, square): return - def test_from_asset(self, alert, gee_dir, asset_france): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_from_asset(self, alert, gee_dir): + # init parameters + asset_id = str(gee_dir / "feature_collection") aoi_model = aoi.AoiModel(alert, folder=gee_dir) # no asset name with pytest.raises(Exception): - aoi_model._from_asset(asset_france) + aoi_model._from_asset(asset_id) # only pathname and all - asset = {"pathname": asset_france, "column": "ALL", "value": None} + asset = {"pathname": asset_id, "column": "ALL", "value": None} aoi_model._from_asset(asset) - assert aoi_model.name == "france" + assert aoi_model.name == "feature_collection" # all params - asset = {"pathname": asset_france, "column": "ADM0_CODE", "value": 85} + asset = {"pathname": asset_id, "column": "data", "value": 0} aoi_model._from_asset(asset) - assert aoi_model.name == "france_ADM0_CODE_85" + assert aoi_model.name == "feature_collection_data_0" # missing value - asset = {"pathname": asset_france, "column": "ADM0_CODE", "value": None} + asset = {"pathname": asset_id, "column": "data", "value": None} + with pytest.raises(Exception): aoi_model._from_asset(asset) return - @pytest.fixture + @pytest.fixture(scope="class") def square(self): """a geojson square around the vatican city""" @@ -329,7 +329,7 @@ def square(self): ], } - @pytest.fixture + @pytest.fixture(scope="class") def fake_points(self, tmp_dir): """create a fake point file the tmp file will be destroyed after the tests""" @@ -345,7 +345,7 @@ def fake_points(self, tmp_dir): return - @pytest.fixture + @pytest.fixture(scope="class") def fake_vector(self, tmp_dir): """create a fake vector file from the GADM definition of vatican city and save it in the tmp dir. the tmp files will be destroyed after the test.""" @@ -370,12 +370,14 @@ def fake_vector(self, tmp_dir): return @pytest.fixture - def aoi_model_france(self, alert, gee_dir, asset_france): - """create a dummy alert and a test aoi model based on GEE that use the france asset available on the test account""" - - return aoi.AoiModel(alert, asset=asset_france, folder=gee_dir) - - @pytest.fixture + def test_model(self, alert, gee_dir): + """ + Create a test AoiModel based on GEE using Vatican + """ + admin = 110 # vatican city (smalest adm0 feature) + return aoi.AoiModel(alert, admin=admin, folder=gee_dir) + + @pytest.fixture(scope="class") def aoi_model_traits(self): """return the list of an aoi model traits""" @@ -389,7 +391,7 @@ def aoi_model_traits(self): "name", ] - @pytest.fixture + @pytest.fixture(scope="class") def aoi_model_outputs(self): """return the list of an aoi model outputs""" @@ -400,3 +402,27 @@ def aoi_model_outputs(self): "selected_feature", "dst_asset_id", ] + + @pytest.fixture(scope="class") + def test_columns(self): + """return the column of the test vatican aoi""" + return [ + "ADM0_CODE", + "ADM0_NAME", + "DISP_AREA", + "EXP0_YEAR", + "STATUS", + "STR0_YEAR", + "Shape_Leng", + ] + + @pytest.fixture(scope="class") + def test_bounds(self): + """return the bounds of the vatican asset""" + + return ( + 12.445770205631668, + 41.90021953934405, + 12.457671530175347, + 41.90667181034752, + ) diff --git a/tests/test_AoiTile.py b/tests/test_AoiTile.py index 839488e9..56361d85 100644 --- a/tests/test_AoiTile.py +++ b/tests/test_AoiTile.py @@ -1,15 +1,23 @@ +import ee +import pytest + from sepal_ui import aoi class TestAoiTile: - def test_init(self, gee_dir): - - # default init - tile = aoi.AoiTile(folder=gee_dir) - assert isinstance(tile, aoi.AoiTile) + def test_init(self): # init without ee tile = aoi.AoiTile(gee=False) assert tile.view.model.ee is False return + + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_init_ee(self, gee_dir): + + # default init + tile = aoi.AoiTile(folder=str(gee_dir)) + assert isinstance(tile, aoi.AoiTile) + + return diff --git a/tests/test_AoiView.py b/tests/test_AoiView.py index 6b87e8b2..1ff9ae1c 100644 --- a/tests/test_AoiView.py +++ b/tests/test_AoiView.py @@ -1,3 +1,4 @@ +import ee import pytest from sepal_ui import aoi @@ -6,31 +7,27 @@ class TestAoiView: - def test_init(self, gee_dir): - - # default init - view = aoi.AoiView(folder=gee_dir) - assert isinstance(view, aoi.AoiView) + def test_init(self): # init without ee view = aoi.AoiView(gee=False) assert view.model.ee is False # init with ADMIN - view = aoi.AoiView("ADMIN", folder=gee_dir) + view = aoi.AoiView("ADMIN", gee=False) assert {"header": "CUSTOM"} not in view.w_method.items # init with CUSTOM - view = aoi.AoiView("CUSTOM", folder=gee_dir) + view = aoi.AoiView("CUSTOM", gee=False) assert {"header": "ADMIN"} not in view.w_method.items # init with a list - view = aoi.AoiView(["POINTS"], folder=gee_dir) + view = aoi.AoiView(["POINTS"], gee=False) assert {"text": ms.aoi_sel.points, "value": "POINTS"} in view.w_method.items assert len(view.w_method.items) == 1 + 1 # 1 for the header, 1 for the object # init with a remove list - view = aoi.AoiView(["-POINTS"], folder=gee_dir) + view = aoi.AoiView(["-POINTS"], gee=False) assert {"text": ms.aoi_sel.points, "value": "POINTS"} not in view.w_method.items assert ( len(view.w_method.items) == len(aoi.AoiModel.METHODS) + 2 - 1 @@ -38,37 +35,53 @@ def test_init(self, gee_dir): # init with a mix of both with pytest.raises(Exception): - view = aoi.AoiView(["-POINTS", "DRAW"], folder=gee_dir) + view = aoi.AoiView(["-POINTS", "DRAW"], gee=False) # init with a non existing keyword with pytest.raises(Exception): - view = aoi.AoiView(["TOTO"], folder=gee_dir) + view = aoi.AoiView(["TOTO"], gee=False) # init with a map m = SepalMap(dc=True) - view = aoi.AoiView(map_=m, folder=gee_dir) + view = aoi.AoiView(map_=m, gee=False) assert view.map_ == m # test model name when using view - view = aoi.AoiView(admin=100, folder=gee_dir) - assert view.model.name == "GLP" + view = aoi.AoiView(admin="VAT", gee=False) + assert view.model.name == "VAT" + + return + + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_init_ee(self, gee_dir): + + # default init + view = aoi.AoiView(folder=gee_dir) + assert isinstance(view, aoi.AoiView) + + # test model name when using view + view = aoi.AoiView(admin=110, folder=gee_dir) + assert view.model.name == "VAT" return - def test_admin(self, gee_dir): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_admin_ee(self, gee_dir): # test if admin0 is in Gaul view = aoi.AoiView(folder=gee_dir) first_gaul_item = {"text": "Abyei", "value": 102} assert first_gaul_item == view.w_admin_0.items[0] + return + + def test_admin(self): + # test if admin0 is in gadm view = aoi.AoiView(gee=False) first_gadm_item = {"text": "Afghanistan", "value": "AFG"} assert first_gadm_item == view.w_admin_0.items[0] - return - def test_activate(self, aoi_gee_view): view = aoi_gee_view @@ -98,10 +111,11 @@ def test_activate(self, aoi_gee_view): return - def test_update_aoi(self, aoi_gee_view, aoi_local_view): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_update_gee_aoi(self, aoi_gee_view): - # select Italy - item = next(i for i in aoi_gee_view.w_admin_0.items if i["text"] == "Italy") + # select Vatican + item = next(i for i in aoi_gee_view.w_admin_0.items if i["text"] == "Holy See") aoi_gee_view.w_method.v_model = "ADMIN0" aoi_gee_view.w_admin_0.v_model = item["value"] @@ -110,13 +124,15 @@ def test_update_aoi(self, aoi_gee_view, aoi_local_view): # perform checks assert aoi_gee_view.updated == 1 - assert aoi_gee_view.model.name == "ITA" + assert aoi_gee_view.model.name == "VAT" assert len(aoi_gee_view.map_.layers) == 2 - # same without GEE + def test_update_local_aoi(self, aoi_local_view): - # select Italy - item = next(i for i in aoi_local_view.w_admin_0.items if i["text"] == "Italy") + # select Vatican + item = next( + i for i in aoi_local_view.w_admin_0.items if i["text"] == "Vatican City" + ) aoi_local_view.w_method.v_model = "ADMIN0" aoi_local_view.w_admin_0.v_model = item["value"] @@ -125,28 +141,30 @@ def test_update_aoi(self, aoi_gee_view, aoi_local_view): # perform checks assert aoi_local_view.updated == 1 - assert aoi_local_view.model.name == "ITA" + assert aoi_local_view.model.name == "VAT" assert len(aoi_local_view.map_.layers) == 2 return - def test_reset(self, aoi_gee_view): + def test_reset(self, aoi_local_view): # select Italy - item = next(i for i in aoi_gee_view.w_admin_0.items if i["text"] == "Italy") - aoi_gee_view.w_method.v_model = "ADMIN0" - aoi_gee_view.w_admin_0.v_model = item["value"] + item = next( + i for i in aoi_local_view.w_admin_0.items if i["text"] == "Vatican City" + ) + aoi_local_view.w_method.v_model = "ADMIN0" + aoi_local_view.w_admin_0.v_model = item["value"] # launch the update - aoi_gee_view._update_aoi(None, None, None) + aoi_local_view._update_aoi(None, None, None) # reset - aoi_gee_view.reset() + aoi_local_view.reset() # checks - assert len(aoi_gee_view.map_.layers) == 1 - assert aoi_gee_view.w_method.v_model is None - assert aoi_gee_view.model.name is None + assert len(aoi_local_view.map_.layers) == 1 + assert aoi_local_view.w_method.v_model is None + assert aoi_local_view.model.name is None return @@ -158,7 +176,7 @@ def aoi_gee_view(self, gee_dir): return aoi.AoiView(map_=m, folder=gee_dir) @pytest.fixture - def aoi_local_view(self, gee_dir): + def aoi_local_view(self): """create an AoiView based on GADM with a silent sepalMap""" m = SepalMap(dc=True) diff --git a/tests/test_AssetSelect.py b/tests/test_AssetSelect.py index 42535226..bfe44754 100644 --- a/tests/test_AssetSelect.py +++ b/tests/test_AssetSelect.py @@ -1,24 +1,27 @@ +from pathlib import Path + +import ee import pytest from sepal_ui import sepalwidgets as sw class TestAssetSelect: - def test_init(self, gee_dir): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_init(self, gee_dir, gee_user_dir): # create an asset select that points to the folder I created for testing - asset_select = sw.AssetSelect(folder=gee_dir) - + asset_select = sw.AssetSelect(folder=str(gee_dir)) assert isinstance(asset_select, sw.AssetSelect) - assert "users/bornToBeAlive/sepal_ui_test/france" in asset_select.items + assert str(gee_user_dir / "image") in asset_select.items # create an asset select with an undefined type - asset_select = sw.AssetSelect(folder=gee_dir, types=["toto"]) - + asset_select = sw.AssetSelect(folder=str(gee_dir), types=["toto"]) assert asset_select.items == [] return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_add_default(self, asset_select, default_items): # add a partial list of asset @@ -35,6 +38,7 @@ def test_add_default(self, asset_select, default_items): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_validate(self, asset_select, default_items): # set a legit asset @@ -58,17 +62,25 @@ def test_validate(self, asset_select, default_items): return - def test_check_types(self, asset_select, asset_france): - - # remove the project from asset name - asset_france = asset_france.replace("projects/earthengine-legacy/assets/", "") + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_check_types(self, asset_select, gee_user_dir): # check that the list of asset is complete - assert asset_france in asset_select.items + assert str(gee_user_dir / "image") in asset_select.items + assert str(gee_user_dir / "feature_collection") in asset_select.items + assert ( + str(gee_user_dir / "subfolder/subfolder_feature_collection") + in asset_select.items + ) # set an IMAGE type asset_select.types = ["IMAGE"] - assert asset_france not in asset_select.items + assert str(gee_user_dir / "image") in asset_select.items + assert str(gee_user_dir / "feature_collection") not in asset_select.items + assert ( + str(gee_user_dir / "subfolder/subfolder_feature_collection") + not in asset_select.items + ) # set a type list with a non legit asset type asset_select.types = ["IMAGE", "toto"] @@ -76,26 +88,22 @@ def test_check_types(self, asset_select, asset_france): return - def test_get_items(self, asset_select): - - # Arrange: assume this asset will be always in the GSA - test_asset = "users/bornToBeAlive/sepal_ui_test/italy" + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_get_items(self, asset_select, gee_user_dir): - # Act: test function itself + # test function itself asset_select.items = [] asset_select._get_items() - # Assert - assert test_asset in asset_select.items + assert str(gee_user_dir / "image") in asset_select.items # Test button event - - # Act + # we shoud export an extra asset and check if the new one is here but + # that is 30 extra seconds so we cannot afford yet asset_select.items = [] asset_select.fire_event("click:prepend", None) - # Assert - assert test_asset in asset_select.items + assert str(gee_user_dir / "image") in asset_select.items @pytest.fixture def default_items(self): @@ -111,4 +119,12 @@ def default_items(self): def asset_select(self, gee_dir): """create a default assetSelect""" - return sw.AssetSelect(folder=gee_dir) + return sw.AssetSelect(folder=str(gee_dir)) + + @pytest.fixture(scope="class") + def gee_user_dir(self, gee_dir): + """return the path to the gee_dir assets without the project elements""" + + legacy_project = Path("projects/earthengine-legacy/assets") + + return gee_dir.relative_to(legacy_project) diff --git a/tests/test_Banner.py b/tests/test_Banner.py index 7fa8edfa..f67386f5 100644 --- a/tests/test_Banner.py +++ b/tests/test_Banner.py @@ -57,8 +57,6 @@ def test_get_timeout(self, banner): def banner(self): """Return a default dummy Banner""" - msg = "dummy message" - type_ = "warning" - id_ = "test_banner" - - return sw.Banner(msg=msg, type_=type_, id_=id_, persistent=False) + return sw.Banner( + msg="dummy message", type_="warning", id_="test_banner", persistent=False + ) diff --git a/tests/test_Btn.py b/tests/test_Btn.py index 407c0e3b..aeacf999 100644 --- a/tests/test_Btn.py +++ b/tests/test_Btn.py @@ -11,42 +11,73 @@ def test_init(self): btn = sw.Btn() assert btn.color == "primary" assert btn.v_icon.children[0] == "" - assert btn.children[1] == "Click" + assert btn.children[1] == "" # extensive btn - btn = sw.Btn("toto", "fas fa-folder") + btn = sw.Btn("toto", "fa-solid fa-folder") assert btn.children[1] == "toto" assert isinstance(btn.v_icon, v.Icon) - assert btn.v_icon.children[0] == "fas fa-folder" + assert btn.v_icon.children[0] == "fa-solid fa-folder" return - def test_set_icon(self, btn): + def test_toggle_loading(self, btn): + + btn = btn.toggle_loading() + + assert btn.loading is True + assert btn.disabled is True + + btn.toggle_loading() + assert btn.loading is False + assert btn.disabled is False + + return - # new icon - icon = "fas fa-folder" - btn = btn.set_icon(icon) + def test_set_gliph(self, btn): + + # new gliph + gliph = "fa-solid fa-folder" + btn.gliph = gliph assert isinstance(btn.v_icon, v.Icon) - assert btn.v_icon.children[0] == icon + assert btn.v_icon.children[0] == gliph + assert btn.v_icon.left is True # change existing icon - icon = "fas fa-file" - btn.set_icon(icon) - assert btn.v_icon.children[0] == icon + gliph = "fa-solid fa-file" + btn.gliph = gliph + assert btn.v_icon.children[0] == gliph + + # display only the gliph + btn.msg = "" + assert btn.children[1] == "" + assert btn.v_icon.left is False + + # remove all gliph + gliph = "" + btn.gliph = gliph + assert "d-none" in btn.v_icon.class_ + + # assert deprecation + with pytest.deprecated_call(): + sw.Btn(icon="fa-solid fa-folder") return - def test_toggle_loading(self, btn): + def test_set_msg(self, btn): - btn = btn.toggle_loading() + # test the initial text + assert btn.children[1] == "Click" - assert btn.loading is True - assert btn.disabled is True + # update msg + msg = "New message" + btn.msg = msg + assert btn.children[1] == msg - btn.toggle_loading() - assert btn.loading is False - assert btn.disabled is False + # test deprecation notice + with pytest.deprecated_call(): + sw.Btn(text="Deprecation") return @@ -54,4 +85,4 @@ def test_toggle_loading(self, btn): def btn(self): """Create a simple btn""" - return sw.Btn() + return sw.Btn("Click") diff --git a/tests/test_CopyToClip.py b/tests/test_CopyToClip.py index 4be2e6ad..820c542d 100644 --- a/tests/test_CopyToClip.py +++ b/tests/test_CopyToClip.py @@ -10,7 +10,7 @@ def test_init(self): clip = sw.CopyToClip() assert clip.tf.outlined is True assert isinstance(clip.tf.label, str) - assert clip.tf.append_icon == "fas fa-clipboard" + assert clip.tf.append_icon == "fa-solid fa-clipboard" assert clip.tf.v_model is None # clip with extra options @@ -27,7 +27,7 @@ def test_copy(self, clip): # I don't know how to check the clipboard # check the icon change - assert clip.tf.append_icon == "fas fa-clipboard-check" + assert clip.tf.append_icon == "fa-solid fa-clipboard-check" return diff --git a/tests/test_DownloadBtn.py b/tests/test_DownloadBtn.py index d7e978c2..ec208ed2 100644 --- a/tests/test_DownloadBtn.py +++ b/tests/test_DownloadBtn.py @@ -12,7 +12,7 @@ def test_init(self, file_start): btn = sw.DownloadBtn(txt) assert isinstance(btn, sw.DownloadBtn) - assert btn.children[0].children[0] == "fas fa-download" + assert btn.children[0].children[0] == "fa-solid fa-download" assert btn.children[1] == txt assert file_start in btn.href assert "#" in btn.href diff --git a/tests/test_DrawControl.py b/tests/test_DrawControl.py index 8a14697c..7ffaee89 100644 --- a/tests/test_DrawControl.py +++ b/tests/test_DrawControl.py @@ -33,7 +33,7 @@ def test_hide(self): m = sm.SepalMap() draw_control = sm.DrawControl(m) - m.add_control(draw_control) + m.add(draw_control) # remove it draw_control.hide() diff --git a/tests/test_DrawerItem.py b/tests/test_DrawerItem.py index 1a34d307..d9186dfd 100644 --- a/tests/test_DrawerItem.py +++ b/tests/test_DrawerItem.py @@ -10,13 +10,13 @@ class TestDrawerItem: def test_init_cards(self): title = "toto" id_ = "toto_id" - icon = "fas fa-folder" + icon = "fa-solid fa-folder" # default init drawerItem = sw.DrawerItem(title) assert isinstance(drawerItem, v.ListItem) assert isinstance(drawerItem.children[0].children[0], v.Icon) - assert drawerItem.children[0].children[0].children[0] == "far fa-folder" + assert drawerItem.children[0].children[0].children[0] == "fa-regular fa-folder" assert isinstance(drawerItem.children[1].children[0], v.ListItemTitle) assert drawerItem.children[1].children[0].children[0] == title @@ -64,31 +64,29 @@ def test_display_tile(self): return - @pytest.fixture - def model(self): - class TestModel(Model): - app_ready = Bool(False).tag(sync=True) - - return TestModel() - - def test_add_notif(self, model): - - drawer_item = sw.DrawerItem("title", model=model, bind_var="app_ready") + def test_add_notif(self, model, drawer_item): model.app_ready = True - assert drawer_item.alert_badge in drawer_item.children model.app_ready = False - assert drawer_item.alert_badge not in drawer_item.children - def test_remove_notif(self, model): - - drawer_item = sw.DrawerItem("title", model=model, bind_var="app_ready") + def test_remove_notif(self, model, drawer_item): model.app_ready = True - drawer_item.remove_notif() - assert drawer_item.alert_badge not in drawer_item.children + + @pytest.fixture + def model(self): + class TestModel(Model): + app_ready = Bool(False).tag(sync=True) + + return TestModel() + + @pytest.fixture + def drawer_item(self, model): + """create dummy drawer item""" + + return sw.DrawerItem("title", model=model, bind_var="app_ready") diff --git a/tests/test_EELayer.py b/tests/test_EELayer.py index 8782f5f5..16af5d32 100644 --- a/tests/test_EELayer.py +++ b/tests/test_EELayer.py @@ -1,11 +1,11 @@ import ee +import pytest from sepal_ui import mapping as sm -from sepal_ui.scripts import utils as su class TestEELayer: - @su.need_ee + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_init(self): # create a point gee layer (easier to check) diff --git a/tests/test_FileInput.py b/tests/test_FileInput.py index bcf0f533..223d6688 100644 --- a/tests/test_FileInput.py +++ b/tests/test_FileInput.py @@ -32,17 +32,22 @@ def test_init(self, root_dir): return def test_bind(self, file_input): - class Test_io(Model): + + # init a model + class TestModel(Model): out = Any(None).tag(sync=True) - test_io = Test_io() + model = TestModel() - test_io.bind(file_input, "out") + # bind the model to the fileinput + model.bind(file_input, "out") + # edit the widget path = "toto.ici.shp" file_input.v_model = path - assert test_io.out == path + # check that the bind worked as expected + assert model.out == path assert file_input.file_menu.v_model is False return @@ -91,6 +96,7 @@ def test_reset(self, file_input, root_dir, readme): # move into sepal_ui folders file_input.select_file(readme) + # reset to default file_input.reset() # assert that the folder has been reset @@ -119,12 +125,19 @@ def file_input(self, root_dir): return sw.FileInput(folder=root_dir) + @pytest.fixture + def readme(self, root_dir): + """return the readme file path""" + + return root_dir / "README.rst" + @staticmethod def get_names(widget): """get the list name of a fileinput object""" - name_list = [] - for item_list in widget.file_list.children[0].children: - name_list.append(item_list.children[1].children[0].children[0]) + item_list = widget.file_list.children[0].children + + def get_name(item): + return item.children[1].children[0].children[0] - return name_list + return [get_name(i) for i in item_list] diff --git a/tests/test_Frontend.py b/tests/test_Frontend.py index 5e2d4088..c1c7ef59 100644 --- a/tests/test_Frontend.py +++ b/tests/test_Frontend.py @@ -12,6 +12,8 @@ def test_init(self): sns = SepalColor() assert isinstance(sns, SimpleNamespace) + return + def test_conf(self): """Check configuration file theme after changing theme""" @@ -25,41 +27,41 @@ def test_conf(self): # Check that light theme is activated in the object assert dark_theme == color._dark_theme - # act # change color theme color - color._dark_theme = True # Be sure now dark theme is stored in conf assert config.get("sepal-ui", "theme") == "dark" + return + def test_repr_html(self): # Arrange expected_title_dark = "

Current theme: dark

" expected_dark = "primary
#b3842e" - # Act + # select dark theme color = SepalColor() color._dark_theme = True + # read the html result and assert that they look like expected html = color._repr_html_().__str__() html = re.sub(r"[\n] [ ]+", "", html) - - # Assert assert expected_title_dark in html assert expected_dark in html - # Arrange - + # same for light theme expected_title_light = "

Current theme: light

" expected_light = "primary
#1976D2" - # Act + # select light theme color._dark_theme = False + + # read html and assert the values of some produced items html = color._repr_html_().__str__() html = re.sub(r"[\n] [ ]+", "", html) - - # Assert assert expected_title_light in html assert expected_light in html + + return diff --git a/tests/test_FullScreenControl.py b/tests/test_FullScreenControl.py index 148242b8..fa29a63d 100644 --- a/tests/test_FullScreenControl.py +++ b/tests/test_FullScreenControl.py @@ -9,12 +9,12 @@ def test_init(self): # add a fullscreenControl control = sm.FullScreenControl(map_) - map_.add_control(control) + map_.add(control) assert isinstance(control, sm.FullScreenControl) assert control in map_.controls assert control.zoomed is False - assert "fas fa-expand" in control.w_btn.children[0].children + assert "fa-solid fa-expand" in control.w_btn.children[0].children return @@ -22,19 +22,19 @@ def test_toggle_fullscreen(self): map_ = sm.SepalMap() control = sm.FullScreenControl(map_) - map_.add_control(control) + map_.add(control) # trigger the click # I cannot test the javascript but i can test everything else control.toggle_fullscreen(None, None, None) assert control.zoomed is True - assert "fas fa-compress" in control.w_btn.children[0].children + assert "fa-solid fa-compress" in control.w_btn.children[0].children # click again to reset to initial state control.toggle_fullscreen(None, None, None) assert control.zoomed is False - assert "fas fa-expand" in control.w_btn.children[0].children + assert "fa-solid fa-expand" in control.w_btn.children[0].children return diff --git a/tests/test_LayerStateControl.py b/tests/test_LayerStateControl.py index 1d0812d9..e0c403a5 100644 --- a/tests/test_LayerStateControl.py +++ b/tests/test_LayerStateControl.py @@ -4,7 +4,6 @@ from traitlets import Bool from sepal_ui import mapping as sm -from sepal_ui.scripts import utils as su class TestLayerStateControl: @@ -12,14 +11,14 @@ def test_init(self): m = sm.SepalMap() state = sm.LayerStateControl(m) - m.add_control(state) + m.add(state) assert isinstance(state, sm.LayerStateControl) assert state.w_state.loading is False return - @su.need_ee + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_update_nb_layer(self, map_with_layers): # create the map and controls @@ -35,6 +34,7 @@ def test_update_nb_layer(self, map_with_layers): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_update_loading(self, map_with_layers): # get the map and control @@ -54,13 +54,13 @@ def test_update_loading(self, map_with_layers): return @pytest.fixture - def map_with_layers(self, fake_layer): + def map_with_layers(self): """create a map with 2 layers and a stateBar""" # create the map and controls m = sm.SepalMap() state = sm.LayerStateControl(m) - m.add_control(state) + m.add(state) # add some ee_layer (loading very fast) # world lights @@ -70,15 +70,13 @@ def map_with_layers(self, fake_layer): m.addLayer(dataset, {}, "Nighttime Lights") # a fake layer with loading update possibilities - m.add_layer(fake_layer) + m.add_layer(self.FakeLayer()) return m - @pytest.fixture - def fake_layer(self): - """create a layer from a fakelayer class that have only one parameter: the laoding trait""" - - class FakeLayer(RasterLayer): - loading = Bool(False).tag(sync=True) + class FakeLayer(RasterLayer): + """ + layer class that have only one parameter: the laoding trait + """ - return FakeLayer() + loading = Bool(False).tag(sync=True) diff --git a/tests/test_LoadTableField.py b/tests/test_LoadTableField.py index ac6392a8..8521571b 100644 --- a/tests/test_LoadTableField.py +++ b/tests/test_LoadTableField.py @@ -63,7 +63,7 @@ def load_table(self): return sw.LoadTableField() - @pytest.fixture + @pytest.fixture(scope="class") def fake_table(self, tmp_dir): """create a fake table""" @@ -85,7 +85,7 @@ def fake_table(self, tmp_dir): return - @pytest.fixture + @pytest.fixture(scope="class") def wrong_table(self, tmp_dir): """create a wrongly defined table (with 2 columns instead of the minimal 3""" diff --git a/tests/test_MapBtn.py b/tests/test_MapBtn.py index 9a9d9c2f..49442f01 100644 --- a/tests/test_MapBtn.py +++ b/tests/test_MapBtn.py @@ -5,11 +5,11 @@ class TestMapBtn: def test_init(self): - # fas icon - map_btn = sm.MapBtn("fas fa-folder") + # fa-solid icon + map_btn = sm.MapBtn("fa-solid fa-folder") assert isinstance(map_btn, sm.MapBtn) assert isinstance(map_btn.children[0], sw.Icon) - assert map_btn.children[0].children[0] == "fas fa-folder" + assert map_btn.children[0].children[0] == "fa-solid fa-folder" # mdi icon map_btn = sm.MapBtn("mdi-folder") diff --git a/tests/test_MenuControl.py b/tests/test_MenuControl.py index eec567cc..c628e82a 100644 --- a/tests/test_MenuControl.py +++ b/tests/test_MenuControl.py @@ -13,7 +13,7 @@ def test_init(self): # create the menu_control m = sm.SepalMap() tile_control = sm.MenuControl("tutu", tile) - m.add_control(tile_control) + m.add(tile_control) # set some object in variables for easy access btn = tile_control.widget.v_slots[0]["children"] @@ -35,7 +35,7 @@ def test_init(self): def test_update_position(self): # create the widget - menu_control = sm.MenuControl("fas fa-folder", sw.Card()) + menu_control = sm.MenuControl("fa-solid fa-folder", sw.Card()) assert menu_control.menu.top is True assert menu_control.menu.bottom is False @@ -56,12 +56,12 @@ def test_close_others(self): # add controls on the map m = sm.SepalMap() - control_1 = sm.MenuControl("fas fa-folder", sw.Card(), m=m) - control_2 = sm.MenuControl("fas fa-folder", sw.Card(), m=m) - control_3 = sm.MenuControl("fas fa-folder", sw.Card()) - m.add_control(control_1) - m.add_control(control_2) - m.add_control(control_3) + control_1 = sm.MenuControl("fa-solid fa-folder", sw.Card(), m=m) + control_2 = sm.MenuControl("fa-solid fa-folder", sw.Card(), m=m) + control_3 = sm.MenuControl("fa-solid fa-folder", sw.Card()) + m.add(control_1) + m.add(control_2) + m.add(control_3) # open the first one and then the second one control_1.menu.v_model = True diff --git a/tests/test_PlanetModel.py b/tests/test_PlanetModel.py index be4d1371..d6d63c5a 100644 --- a/tests/test_PlanetModel.py +++ b/tests/test_PlanetModel.py @@ -9,22 +9,17 @@ @pytest.mark.skipif("PLANET_API_KEY" not in os.environ, reason="requires Planet") class TestPlanetModel: - @pytest.fixture - def planet_key(self): - return os.getenv("PLANET_API_KEY") - - @pytest.fixture - def cred(self): - - credentials = json.loads(os.getenv("PLANET_API_CREDENTIALS")) + def test_init(self, planet_key, cred, request): - return list(credentials.values()) + # Test with a valid api key + planet_model = PlanetModel(planet_key) - @pytest.mark.parametrize("credentials", ["planet_key", "cred"]) - def test_init(self, credentials, request): + assert isinstance(planet_model, PlanetModel) + assert isinstance(planet_model.session, planet.http.Session) + assert planet_model.active is True - # Test with a valid api key and login credentials - planet_model = PlanetModel(request.getfixturevalue(credentials)) + # Test with a valid login credentials + planet_model = PlanetModel(cred) assert isinstance(planet_model, PlanetModel) assert isinstance(planet_model.session, planet.http.Session) @@ -34,6 +29,8 @@ def test_init(self, credentials, request): with pytest.raises(Exception): planet_model = PlanetModel("not valid") + return + @pytest.mark.parametrize("credentials", ["planet_key", "cred"]) def test_init_client(self, credentials, request): @@ -45,6 +42,8 @@ def test_init_client(self, credentials, request): with pytest.raises(Exception): planet_model.init_session("wrongkey") + return + def test_init_session_from_event(self): planet_model = PlanetModel() @@ -61,10 +60,9 @@ def test_init_session_from_event(self): with pytest.raises(planet.exceptions.APIError): planet_model.init_session(["valid@email.format", "not_exists"]) - def test_is_active(self, planet_key): + return - # We only need to test with a key. - planet_model = PlanetModel(planet_key) + def test_is_active(self, planet_model): planet_model._is_active() assert planet_model.active is True @@ -72,21 +70,21 @@ def test_is_active(self, planet_key): with pytest.raises(Exception): planet_model = PlanetModel("wrongkey") - def test_get_subscriptions(self, planet_key): + return + + def test_get_subscriptions(self, planet_model): - planet_model = PlanetModel(planet_key) subs = planet_model.get_subscriptions() # Check object has length, because there is no way to check a value # that might change over the time. - assert len(subs) + assert len(subs) != 0 - def test_get_planet_items(self, planet_key): + return - # Arrange - planet_model = PlanetModel(planet_key) + def test_get_planet_items(self, planet_model): - aoi = { + aoi = { # Yasuni national park in Ecuador "type": "Polygon", "coordinates": ( ( @@ -105,8 +103,26 @@ def test_get_planet_items(self, planet_key): expected_first_id = "20201118_144642_48_2262" - # Act + # Get the items items = planet_model.get_items(aoi, start, end, cloud_cover) - - # Assert assert items[0].get("id") == expected_first_id + + @pytest.fixture + def planet_key(self): + + return os.getenv("PLANET_API_KEY") + + @pytest.fixture + def cred(self): + + credentials = json.loads(os.getenv("PLANET_API_CREDENTIALS")) + + return list(credentials.values()) + + @pytest.fixture + def planet_model(self): + """Start a planet model using the API key""" + + key = os.getenv("PLANET_API_KEY") + + return PlanetModel(key) diff --git a/tests/test_PlanetView.py b/tests/test_PlanetView.py index bee8dd76..9e140960 100644 --- a/tests/test_PlanetView.py +++ b/tests/test_PlanetView.py @@ -9,80 +9,72 @@ @pytest.mark.skipif("PLANET_API_KEY" not in os.environ, reason="requires Planet") class TestPlanetView: - def test_init(self): - - # With own components - planet_view = PlanetView() + def test_init(self, planet_view): + # check the feault planet_view assert isinstance(planet_view, PlanetView) # With external components external_btn = sw.Btn() external_alert = sw.Btn() - planet_view = PlanetView(btn=external_btn, alert=external_alert) assert planet_view.btn == external_btn assert planet_view.alert == external_alert - def test_reset(self): + return - # Arrange - planet_view = PlanetView() + def test_reset(self, planet_view): + # add dummy parameter planet_view.w_username.v_model = "dummy" planet_view.w_password.v_model = "dummy" planet_view.w_key.v_model = "dummy" - # Act + # reset the view planet_view.reset() + assert planet_view.w_username.v_model is None + assert planet_view.w_password.v_model is None + assert planet_view.w_key.v_model is None - # Assert - planet_view.w_username.v_model is None - planet_view.w_password.v_model is None - planet_view.w_key.v_model is None - - def test_swap(self): - - # Arramge - planet_view = PlanetView() - + # use a default method default_method = "credentials" - - # Assert default assert planet_view.w_method.v_model == default_method assert planet_view.w_username.viz is True assert planet_view.w_password.viz is True assert planet_view.w_key.viz is False - # Act changing method - + # change the method planet_view.w_method.v_model = "api_key" - - # Assert change assert planet_view.w_method.v_model == "api_key" assert planet_view.w_username.viz is False assert planet_view.w_password.viz is False assert planet_view.w_key.viz is True - def test_validate(self): + return + + def test_validate(self, planet_view): # Arrange credentials = tuple(json.loads(os.getenv("PLANET_API_CREDENTIALS")).values()) api_key = os.getenv("PLANET_API_KEY") - planet_view = PlanetView() # Act with credentials planet_view.w_method.v_model = "credentials" planet_view.w_username.v_model, planet_view.w_password.v_model = credentials planet_view.btn.fire_event("click", None) - - # Assert assert planet_view.planet_model.active is True # Act with api_key planet_view.w_method.v_model = "api_key" planet_view.w_key.v_model = api_key planet_view.btn.fire_event("click", None) - assert planet_view.planet_model.active is True + + return + + @pytest.fixture + def planet_view(self): + """default PLanetView""" + + return PlanetView() diff --git a/tests/test_PlanetWidgets.py b/tests/test_PlanetWidgets.py index 856f94f8..636eec9b 100644 --- a/tests/test_PlanetWidgets.py +++ b/tests/test_PlanetWidgets.py @@ -5,40 +5,35 @@ class TestPlanetWidgets: - def test_init(self): + def test_init(self, info_view): model = PlanetModel() info_view = InfoView(model=model) assert isinstance(info_view, InfoView) - def test_open_info(self, no_subs, only_others, only_nicfi, all_subs): + return - model = PlanetModel() - info_view = InfoView(model=model) + def test_open_info(self, no_subs, only_others, only_nicfi, all_subs, info_view): # Trigger event to check subscriptions - model.subscriptions = {} - model.subscriptions = no_subs - + info_view.model.subscriptions = {} + info_view.model.subscriptions = no_subs assert info_view.get_children("nicfi").disabled assert info_view.get_children("others").disabled - model.subscriptions = {} - model.subscriptions = only_others - + info_view.model.subscriptions = {} + info_view.model.subscriptions = only_others assert info_view.get_children("nicfi").disabled assert not info_view.get_children("others").disabled - model.subscriptions = {} - model.subscriptions = only_nicfi - + info_view.model.subscriptions = {} + info_view.model.subscriptions = only_nicfi assert not info_view.get_children("nicfi").disabled assert info_view.get_children("others").disabled - model.subscriptions = {} - model.subscriptions = all_subs - + info_view.model.subscriptions = {} + info_view.model.subscriptions = all_subs assert not info_view.get_children("nicfi").disabled assert not info_view.get_children("others").disabled @@ -56,12 +51,14 @@ def test_open_info(self, no_subs, only_others, only_nicfi, all_subs): info_view.get_children("nicfi").fire_event("click", None) assert info_view.v_model == 1 - @pytest.fixture + return + + @pytest.fixture(scope="class") def no_subs(self): return {"nicfi": [], "others": []} - @pytest.fixture + @pytest.fixture(scope="class") def only_others(self): return { @@ -78,7 +75,7 @@ def only_others(self): ], } - @pytest.fixture + @pytest.fixture(scope="class") def only_nicfi(self): return { @@ -103,7 +100,7 @@ def only_nicfi(self): "others": [], } - @pytest.fixture + @pytest.fixture(scope="class") def all_subs(self): return { @@ -136,3 +133,9 @@ def all_subs(self): } ], } + + @pytest.fixture + def info_view(self): + """InfoView widget""" + + return InfoView(model=PlanetModel()) diff --git a/tests/test_ReclassifyModel.py b/tests/test_ReclassifyModel.py index 3c116ac8..5e436b27 100644 --- a/tests/test_ReclassifyModel.py +++ b/tests/test_ReclassifyModel.py @@ -9,10 +9,10 @@ from sepal_ui import aoi from sepal_ui.reclassify import ReclassifyModel -from sepal_ui.scripts import gee class TestReclassifyModel: + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_gee_init(self, model_gee): assert isinstance(model_gee, ReclassifyModel) @@ -27,14 +27,14 @@ def test_local_init(self, model_local): return - def test_get_classes(self, model_gee, reclass_file): + def test_get_classes(self, model_local, reclass_file): """Test if the matrix is saved and corresponds with the the output""" with pytest.raises(Exception): - model_gee.dst_class_file = "I/dont/exist.nothing" - model_gee.get_classes() + model_local.dst_class_file = "I/dont/exist.nothing" + model_local.get_classes() - # Arrange + # test these class expected_class_dict = { 1: ("Forest", "#044D02"), 2: ("Grassland", "#F5FF00"), @@ -44,15 +44,14 @@ def test_get_classes(self, model_gee, reclass_file): 6: ("Other land", "#FF00DE"), } - # Act - model_gee.dst_class_file = str(reclass_file) - class_dict = model_gee.get_classes() - - # Assert + # load the class file + model_local.dst_class_file = str(reclass_file) + class_dict = model_local.get_classes() assert class_dict == expected_class_dict return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_get_type_gee(self, model_gee, model_gee_vector, model_gee_image): """Tests the asset type with gee""" @@ -87,14 +86,15 @@ def test_get_type_local(self, model_local, model_local_vector, model_local_image return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_get_bands_gee(self, model_gee_vector, model_gee_image): """check if the bands are correctly retreived""" # Arrange - table_bands = ["APOYO", "AREA_HA", "CAMBIO", "CODIGO"] - image_bands = ["y1992", "y1993", "y1994"] + table_bands = ["data"] + image_bands = ["constant"] - # Act - Assert + # assert assert model_gee_vector.get_bands() == table_bands assert model_gee_image.get_bands() == image_bands @@ -110,10 +110,15 @@ def test_get_bands_local(self, model_local_vector, model_local_image): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_get_aoi(self, model_gee): + # tested on model_gee instead of model_local a clipping is not yet + # possible on local gdf and/or raster + # default to error if no asset is set in the aoi_model with pytest.raises(Exception): + model_gee.enforce_aoi = True assert model_gee.get_aoi() is None # Test when there is an aoi but there is not a feature collection selected @@ -121,111 +126,47 @@ def test_get_aoi(self, model_gee): assert model_gee.get_aoi() is None # set the aoi to france - model_gee.aoi_model._from_admin(85) # france + model_gee.aoi_model._from_admin(110) # Vatican city assert model_gee.get_aoi() is not None return - def test_unique_gee_image(self, model_gee_image, asset_image_aoi, no_name): - - # Unique values when not using an area of interes - image_unique = [ - 10, - 11, - 30, - 40, - 50, - 60, - 100, - 110, - 120, - 130, - 150, - 160, - 180, - 190, - 210, - ] + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_unique_gee_image(self, model_gee_image, aoi_model, no_name): - # Unique values when using a sample area of interest - image_unique_aoi = [30, 40, 50, 100, 110, 120, 130, 160] + # read the band of the image + model_gee_image.band = "constant" - model_gee_image.band = "y1992" - model_gee_image.aoi_model._from_asset( - {"pathname": asset_image_aoi, "column": "ALL", "value": None} - ) - print(model_gee_image.aoi_model.name) + # Unique values when using a sample area of interest + image_unique_aoi = [2] + model_gee_image.aoi_model = aoi_model + assert model_gee_image.get_aoi() is not None assert model_gee_image.unique() == {str(i): no_name for i in image_unique_aoi} + # unique value when no aoi is set + # tested after as we delete the aoi_model + image_unique = [1, 2, 3, 4] model_gee_image.aoi_model = None assert model_gee_image.unique() == {str(i): no_name for i in image_unique} return - def test_unique_gee_vector(self, model_gee_vector, asset_table_aoi, no_name): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_unique_gee_vector(self, model_gee_vector, aoi_model, no_name): - # Unique values when not using an area of interest - vector_unique = [ - 111, - 112, - 231, - 232, - 233, - 242, - 243, - 244, - 245, - 313, - 314, - 323, - 333, - 334, - 511, - 512, - 1312, - 2121, - 2141, - 2232, - 3131, - 3132, - 3221, - 3222, - 3231, - 3232, - 31111, - 31121, - 31211, - 31221, - 32111, - 32112, - ] + model_gee_vector.band = "data" # Unique values when using an area of interest - vector_unique_aoi = [ - 231, - 233, - 242, - 243, - 244, - 313, - 323, - 511, - 3131, - 3132, - 3221, - 3231, - 3232, - 31111, - ] - - model_gee_vector.band = "CODIGO" - model_gee_vector.aoi_model._from_asset( - {"pathname": asset_table_aoi, "column": "ALL", "value": None} - ) + vector_unique_aoi = [3] + model_gee_vector.aoi_model = aoi_model + assert model_gee_vector.get_aoi() is not None assert model_gee_vector.unique() == {i: no_name for i in vector_unique_aoi} + # Unique values when not using an area of interest + # tested after as we delete the aoi_model + vector_unique = [0, 1, 2, 3] model_gee_vector.aoi_model = None - model_gee_vector.band = "CODIGO" + model_gee_vector.band = "data" assert model_gee_vector.unique() == {i: no_name for i in vector_unique} return @@ -250,6 +191,7 @@ def test_unique_local_vector(self, model_local_vector, no_name): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_reclassify_initial_exceptions(self, model_gee_image): # Test reclassify method without matrix @@ -263,81 +205,39 @@ def test_reclassify_initial_exceptions(self, model_gee_image): # test reclassify without setting an aoi with pytest.raises(Exception): + model_gee_image.enforce_aoi = True model_gee_image.matrix = {1: 1} - model_gee_image.band = "y1992" + model_gee_image.band = "constant" model_gee_image.reclassify() return - def test_reclassify_gee_vector(self, model_gee_vector, asset_table_aoi, alert): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_reclassify_gee_vector(self, model_gee_vector, gee_dir, alert): """Test reclassification of vectors when using an area of interest""" - unique_value = [ - 231, - 233, - 242, - 243, - 244, - 313, - 323, - 511, - 3131, - 3132, - 3221, - 3231, - 3232, - 31111, - ] + unique_value = [0, 1, 2, 3] matrix = {v: 2 * i // len(unique_value) for i, v in enumerate(unique_value)} model_gee_vector.matrix = matrix - model_gee_vector.band = "CODIGO" - model_gee_vector.aoi_model._from_asset( - {"pathname": asset_table_aoi, "column": "ALL", "value": None} - ) + model_gee_vector.band = "data" model_gee_vector.reclassify() - if model_gee_vector.save: - - assert model_gee_vector.dst_gee == f"{model_gee_vector.src_gee}_reclass" - - # delete the created file - description = Path(model_gee_vector.dst_gee).stem - gee.wait_for_completion(description, alert) - ee.data.deleteAsset(model_gee_vector.dst_gee) - else: - - assert model_gee_vector.dst_gee_memory is not None + assert model_gee_vector.dst_gee_memory is not None return - def test_reclassify_gee_image(self, model_gee_image, asset_image_aoi, alert): + def test_reclassify_gee_image(self, model_gee_image, alert): """Test reclassification of vectors when using an area of interest""" - unique_value = [30, 40, 50, 100, 110, 120, 130, 160] - matrix = { - v: 2 * (i // len(unique_value) + 1) for i, v in enumerate(unique_value) - } + unique_value = [1, 2, 3, 4] + matrix = {v: 2 * i // len(unique_value) for i, v in enumerate(unique_value)} model_gee_image.matrix = matrix - model_gee_image.band = "y1992" - model_gee_image.aoi_model._from_asset( - {"pathname": asset_image_aoi, "column": "ALL", "value": None} - ) + model_gee_image.band = "constant" model_gee_image.reclassify() - if model_gee_image.save: - - assert model_gee_image.dst_gee == f"{model_gee_image.src_gee}_reclass" - - # delete the created file - description = Path(model_gee_image.dst_gee).stem - gee.wait_for_completion(description, alert) - ee.data.deleteAsset(model_gee_image.dst_gee) - - else: - - assert model_gee_image.dst_gee_memory is not None + assert model_gee_image.dst_gee_memory is not None return @@ -379,7 +279,7 @@ def test_reclassify_local_vector(self, model_local_vector, tmp_dir): return - @pytest.fixture + @pytest.fixture(scope="class") def reclass_file(self, tmp_dir): """create a fake classification file""" @@ -403,34 +303,26 @@ def reclass_file(self, tmp_dir): return @pytest.fixture - def model_gee(self, tmp_dir, alert, gee_ready, gee_dir): + def model_gee(self, tmp_dir, alert, gee_dir): """Reclassify model using Google Earth Engine assets""" - aoi_model = aoi.AoiModel(alert, gee=True, folder=gee_dir) + aoi_model = aoi.AoiModel(gee=True, folder=gee_dir) return ReclassifyModel( - enforce_aoi=True, + enforce_aoi=False, gee=True, dst_dir=tmp_dir, aoi_model=aoi_model, - save=gee_ready, + save=False, folder=gee_dir, ) - @pytest.fixture - def model_local(self, tmp_dir, alert): - """Reclassify model using local raster assets""" - - aoi_model = aoi.AoiModel(alert, gee=False) - - return ReclassifyModel(gee=False, dst_dir=tmp_dir, aoi_model=aoi_model) - @pytest.fixture def model_gee_vector(self, model_gee, gee_dir): """Creates a reclassify model with a gee vector""" model_gee = deepcopy(model_gee) - model_gee.src_gee = f"{gee_dir}/reclassify_table" + model_gee.src_gee = str(gee_dir / "feature_collection") model_gee.get_type() return model_gee @@ -440,11 +332,19 @@ def model_gee_image(self, model_gee, gee_dir): """Creates a reclassify model with a gee image""" model_gee = deepcopy(model_gee) - model_gee.src_gee = f"{gee_dir}/reclassify_image" + model_gee.src_gee = str(gee_dir / "image") model_gee.get_type() return model_gee + @pytest.fixture + def model_local(self, tmp_dir, alert): + """Reclassify model using local raster assets""" + + aoi_model = aoi.AoiModel(gee=False) + + return ReclassifyModel(gee=False, dst_dir=tmp_dir, aoi_model=aoi_model) + @pytest.fixture def model_local_vector(self, model_local, tmp_dir): """Create a reclassify model with a local vector""" @@ -485,3 +385,26 @@ def model_local_image(self, model_local, tmp_dir): filename.unlink() return + + @pytest.fixture + def no_name(self): + """return a no-name tuple""" + + return ("no_name", "#000000") + + @pytest.fixture(scope="class") + def aoi_model(self, _hash, gee_dir): + """create an aoi_model with a 100m square geometry centered in 50, 50""" + + # create the geoemtry as featurecollection + point = ee.Geometry.Point([50, 50], "EPSG:3857") + aoi_ee = ee.FeatureCollection(point.buffer(50).bounds()) + aoi_gdf = gpd.GeoDataFrame.from_features(aoi_ee.getInfo()) + + # add it to a aoi_model + aoi_model = aoi.AoiModel(gee=True, folder=gee_dir) + aoi_model.feature_collection = aoi_ee + aoi_model.gdf = aoi_gdf + aoi_model.name = f"test_{_hash}" + + return aoi_model diff --git a/tests/test_ReclassifyTile.py b/tests/test_ReclassifyTile.py index 4f6b8bc8..628f19b4 100644 --- a/tests/test_ReclassifyTile.py +++ b/tests/test_ReclassifyTile.py @@ -1,15 +1,23 @@ from pathlib import Path +import ee +import pytest + from sepal_ui import reclassify as rec class TestReclassifyTile: - def test_init(self, gee_dir): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_init_gee(self, gee_dir): # default init tile = rec.ReclassifyTile(Path.home(), gee=True, folder=gee_dir) assert isinstance(tile, rec.ReclassifyTile) + return + + def test_init(self): + # init without ee tile = rec.ReclassifyTile(Path.home(), gee=False) assert tile.model.gee is False diff --git a/tests/test_ReclassifyView.py b/tests/test_ReclassifyView.py index ba1449f4..2d9af1a4 100644 --- a/tests/test_ReclassifyView.py +++ b/tests/test_ReclassifyView.py @@ -1,6 +1,7 @@ from pathlib import Path from zipfile import ZipFile +import ee import geopandas as gpd import pytest @@ -9,10 +10,11 @@ class TestReclassifyView: + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_init_exception(alert, gee_dir): """Test exceptions""" - aoi_model = aoi.AoiModel(alert, gee=False) + aoi_model = aoi.AoiModel(gee=False) # aoi_model has to be local when using local view. with pytest.raises(Exception): @@ -161,18 +163,18 @@ def view_local(self, tmp_dir, class_file, alert): def view_gee(self, tmp_dir, class_file, gee_dir, alert): """return a gee reclassify view""" - aoi_model = aoi.AoiModel(alert, gee=True, folder=gee_dir) + aoi_model = aoi.AoiModel(alert, gee=True, folder=str(gee_dir)) return ReclassifyView( aoi_model=aoi_model, gee=True, - folder=gee_dir, + folder=str(gee_dir), out_path=tmp_dir, class_path=tmp_dir, default_class={"IPCC": str(class_file)}, ) - @pytest.fixture + @pytest.fixture(scope="class") def class_file(self, tmp_dir): file = tmp_dir / "dum_default_classes.csv" @@ -191,7 +193,7 @@ def class_file(self, tmp_dir): return - @pytest.fixture + @pytest.fixture(scope="class") def map_file_bad_char(self, tmp_dir): bad_file = tmp_dir / "map_file_bad_char.csv" @@ -203,7 +205,7 @@ def map_file_bad_char(self, tmp_dir): return - @pytest.fixture + @pytest.fixture(scope="class") def map_file_bad_header(self, tmp_dir): bad_file = tmp_dir / "map_file_bad_header.csv" @@ -215,7 +217,7 @@ def map_file_bad_header(self, tmp_dir): return - @pytest.fixture + @pytest.fixture(scope="class") def map_file(self, tmp_dir): file = tmp_dir / "map_file.csv" @@ -233,10 +235,10 @@ def map_file(self, tmp_dir): return - @pytest.fixture - def model_local_vector(self, tmp_dir, alert): + @pytest.fixture(scope="class") + def model_local_vector(self, tmp_dir): - aoi_model = aoi.AoiModel(alert, gee=False) + aoi_model = aoi.AoiModel(gee=False) # create the vector file file = Path(gpd.datasets.get_path("nybb").replace("zip:", "")) diff --git a/tests/test_SepalMap.py b/tests/test_SepalMap.py index 979a6955..89612376 100644 --- a/tests/test_SepalMap.py +++ b/tests/test_SepalMap.py @@ -87,6 +87,7 @@ def test_set_center(self): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def zoom_ee_object(self): # init objects @@ -130,8 +131,7 @@ def test_zoom_bounds(self): return @pytest.mark.skipif( - is_set_localtileserver is False, - reason="localtileserver implementation is still in beta", + is_set_localtileserver is False, reason="localtileserver in beta" ) def test_add_raster(self, rgb, byte): @@ -158,6 +158,7 @@ def test_add_colorbar(self): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_add_ee_layer_exceptions(self): map_ = sm.SepalMap() @@ -183,10 +184,13 @@ def test_add_ee_layer_exceptions(self): with pytest.raises(AttributeError): map_.addLayer(geometry, {"invalid_propery": "red", "fillColor": None}) - def test_add_ee_layer(self, asset_image_viz): + return + + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_add_ee_layer(self, image_id): # create map and image - image = ee.Image(asset_image_viz) + image = ee.Image(image_id) m = sm.SepalMap() # display all the viz available in the image @@ -243,10 +247,10 @@ def test_get_basemap_list(self): return - def test_get_viz_params(self, asset_image_viz): - - image = ee.Image(asset_image_viz) + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_get_viz_params(self, image_id): + image = ee.Image(image_id) res = sm.SepalMap.get_viz_params(image) expected = { @@ -296,6 +300,7 @@ def test_get_viz_params(self, asset_image_viz): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_remove_layer(self, ee_map_with_layers): m = ee_map_with_layers @@ -312,6 +317,7 @@ def test_remove_layer(self, ee_map_with_layers): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_remove_all(self, ee_map_with_layers): m = ee_map_with_layers @@ -379,6 +385,8 @@ def test_add_layer(self): assert new_layer.style == layer_style assert new_layer.hover_style == layer_hover_style + return + def test_add_basemap(self): m = sm.SepalMap() @@ -403,6 +411,7 @@ def test_get_scale(self): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_find_layer(self, ee_map_with_layers): m = ee_map_with_layers @@ -444,8 +453,7 @@ def test_find_layer(self, ee_map_with_layers): return @pytest.mark.skipif( - is_set_localtileserver is False, - reason="localtileserver implementation is still in beta", + is_set_localtileserver is False, reason="localtileserver is in beta" ) def test_zoom_raster(self, byte): @@ -459,6 +467,7 @@ def test_zoom_raster(self, byte): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_add_legend(self, ee_map_with_layers): legend_dict = { @@ -478,7 +487,7 @@ def test_add_legend(self, ee_map_with_layers): return - @pytest.fixture + @pytest.fixture(scope="class") def rgb(self): """add a raster file of the bahamas coming from rasterio test suit""" @@ -494,7 +503,7 @@ def rgb(self): return - @pytest.fixture + @pytest.fixture(scope="class") def byte(self): """add a raster file of the bahamas coming from rasterio test suit""" @@ -511,9 +520,9 @@ def byte(self): return @pytest.fixture - def ee_map_with_layers(self, asset_image_viz): + def ee_map_with_layers(self, image_id): - image = ee.Image(asset_image_viz) + image = ee.Image(image_id) m = sm.SepalMap() # display all the viz available in the image @@ -521,3 +530,10 @@ def ee_map_with_layers(self, asset_image_viz): m.addLayer(image, {}, viz["name"], viz_name=viz["name"]) return m + + @pytest.fixture(scope="class") + def image_id(self): + + # testing asset from Daniel Wiell + # may not live forever + return "users/wiell/forum/visualization_example" diff --git a/tests/test_StateIcon.py b/tests/test_StateIcon.py index 22963cac..d261f442 100644 --- a/tests/test_StateIcon.py +++ b/tests/test_StateIcon.py @@ -7,15 +7,6 @@ class TestStateIcon: - @pytest.fixture - def model(self): - """Dummy model with state value trait""" - - class TestModel(Model): - state_value = Unicode().tag(sync=True) - - return TestModel() - def test_init(self, model): # Test with default states @@ -25,35 +16,34 @@ def test_init(self, model): assert state_icon.children[0] == "Valid" # Test with custom states - - # Arrange custom_states = { "off": ("Non connected", color.darker), "init": ("Initializing...", color.warning), "failed": ("Connection failed!", color.error), "successfull": ("Successfull", color.success), } - - # Act state_icon = sw.StateIcon(model, "state_value", custom_states) - # Assert assert state_icon.icon.color == color.darker assert state_icon.children[0] == "Non connected" def test_swap(self, model): - # Arrange state_icon = sw.StateIcon(model, "state_value") - - # Act model.state_value = "non_valid" - # Assert assert state_icon.icon.color == color.error assert state_icon.children[0] == "Not valid" # Test raise exception - with pytest.raises(ValueError): model.state_value = "asdf" + + @pytest.fixture + def model(self): + """Dummy model with state value trait""" + + class TestModel(Model): + state_value = Unicode().tag(sync=True) + + return TestModel() diff --git a/tests/test_Translator.py b/tests/test_Translator.py index 03e44fb6..b25bb8b8 100644 --- a/tests/test_Translator.py +++ b/tests/test_Translator.py @@ -177,7 +177,7 @@ def translation_folder(self): return - @pytest.fixture(scope="function") + @pytest.fixture def tmp_config_file(self): """ Erase any existing config file and replace it with one specifically diff --git a/tests/test_ValueInspector.py b/tests/test_ValueInspector.py index 642851e4..9e5cfd64 100644 --- a/tests/test_ValueInspector.py +++ b/tests/test_ValueInspector.py @@ -7,7 +7,6 @@ import pytest from sepal_ui import mapping as sm -from sepal_ui.scripts import utils as su class TestValueInspector: @@ -15,7 +14,7 @@ def test_init(self): m = sm.SepalMap() value_inspector = sm.ValueInspector(m) - m.add_control(value_inspector) + m.add(value_inspector) assert isinstance(value_inspector, sm.ValueInspector) @@ -23,7 +22,7 @@ def test_toogle_cursor(self): m = sm.SepalMap() value_inspector = sm.ValueInspector(m) - m.add_control(value_inspector) + m.add(value_inspector) # activate the window value_inspector.menu.v_model = True @@ -40,7 +39,7 @@ def test_read_data(self): # not testing the display of anything here just the interaction m = sm.SepalMap() value_inspector = sm.ValueInspector(m) - m.add_control(value_inspector) + m.add(value_inspector) # click anywhere without activation value_inspector.read_data(type="click", coordinates=[0, 0]) @@ -53,7 +52,7 @@ def test_read_data(self): return - @su.need_ee + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_free_eelayer(self, world_temp, ee_adm2): # create a map with a value inspector @@ -112,7 +111,7 @@ def test_from_raster(self, raster_bahamas): return - @pytest.fixture + @pytest.fixture(scope="class") def world_temp(self): """get the world temperature dataset from GEE""" @@ -122,13 +121,13 @@ def world_temp(self): .select("temperature_2m") ) - @pytest.fixture + @pytest.fixture(scope="class") def ee_adm2(self): """get a featurecollection with only adm2code values""" return ee.FeatureCollection("FAO/GAUL/2015/level2").select("ADM2_CODE") - @pytest.fixture + @pytest.fixture(scope="class") def raster_bahamas(self): """add a raster file of the bahamas coming from rasterio test suit""" @@ -144,7 +143,7 @@ def raster_bahamas(self): return - @pytest.fixture + @pytest.fixture(scope="class") def adm0_vatican(self): """create a geojson of vatican city""" diff --git a/tests/test_VectorField.py b/tests/test_VectorField.py index e6ee32d5..34547e25 100644 --- a/tests/test_VectorField.py +++ b/tests/test_VectorField.py @@ -1,6 +1,7 @@ from urllib.request import urlretrieve from zipfile import ZipFile +import ee import pytest from sepal_ui import sepalwidgets as sw @@ -32,17 +33,18 @@ def test_update_file(self, vector_field, fake_vector, default_v_model): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_update_file_gee(self, vector_field_gee, default_v_model, fake_asset): # Arrange test_data = { - "pathname": fake_asset, + "pathname": str(fake_asset), "column": "ALL", "value": None, } # Act - vector_field_gee._update_file({"new": fake_asset}) + vector_field_gee._update_file({"new": str(fake_asset)}) # Assert assert vector_field_gee.v_model == test_data @@ -65,10 +67,11 @@ def test_reset(self, vector_field, fake_vector, default_v_model): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_reset_gee(self, vector_field_gee, default_v_model, fake_asset): - # It will trigger the - vector_field_gee.w_file.v_model = fake_asset + # It will trigger the event + vector_field_gee.w_file.v_model = str(fake_asset) # reset the loadtable vector_field_gee.reset() @@ -91,16 +94,17 @@ def test_update_column(self, vector_field, fake_vector): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_update_column_gee(self, vector_field_gee, fake_asset): # change the value of the file - vector_field_gee._update_file({"new": fake_asset}) + vector_field_gee._update_file({"new": str(fake_asset)}) # read a column - vector_field_gee.w_column.v_model = "CAMBIO" - assert vector_field_gee.v_model["column"] == "CAMBIO" + vector_field_gee.w_column.v_model = "data" + assert vector_field_gee.v_model["column"] == "data" assert "d-none" not in vector_field_gee.w_value.class_ - assert vector_field_gee.w_value.items == [0, 1, 2, 3, 4, 5, 6, 7] + assert vector_field_gee.w_value.items == [0, 1, 2, 3] return @@ -117,22 +121,23 @@ def test_update_value(self, vector_field, fake_vector): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_update_value_gee(self, vector_field_gee, fake_asset): # change the value of the file - vector_field_gee._update_file({"new": fake_asset}) + vector_field_gee._update_file({"new": str(fake_asset)}) # read a column - vector_field_gee.w_column.v_model = "CAMBIO" + vector_field_gee.w_column.v_model = "data" vector_field_gee.w_value.v_model = 1 assert vector_field_gee.v_model["value"] == 1 return - @pytest.fixture + @pytest.fixture(scope="class") def default_v_model(self): - """Returns default v_model""" + """Returns the default v_model""" return { "pathname": None, @@ -152,7 +157,7 @@ def vector_field_gee(self, gee_dir): return sw.VectorField(gee=True, folder=gee_dir) - @pytest.fixture + @pytest.fixture(scope="class") def fake_vector(self, tmp_dir): """return a fake vector based on the vatican file""" @@ -176,8 +181,8 @@ def fake_vector(self, tmp_dir): return - @pytest.fixture + @pytest.fixture(scope="class") def fake_asset(self, gee_dir): - """Returns a fake asset""" + """return the path to a fake asset""" - return f"{gee_dir}/reclassify_table" + return gee_dir / "feature_collection" diff --git a/tests/test_decorator.py b/tests/test_decorator.py index f466b25f..f629e097 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -1,5 +1,6 @@ import warnings +import ee import ipyvuetify as v import pytest @@ -9,6 +10,14 @@ class TestDecorator: + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_init_ee(self): + + # check that no error is raised + sd.init_ee() + + return + def test_catch_errors(self): # create a fake object that uses the decorator @@ -159,11 +168,3 @@ def func6(self, *args): obj.func6() return - - @sd.need_ee - def test_init_ee(self): - - # check that no error is raised - sd.init_ee() - - return diff --git a/tests/test_gee.py b/tests/test_gee.py index 28c411fe..9b16e569 100644 --- a/tests/test_gee.py +++ b/tests/test_gee.py @@ -1,4 +1,3 @@ -import os import time import ee @@ -6,32 +5,26 @@ from sepal_ui.message import ms from sepal_ui.scripts import gee -from sepal_ui.scripts import utils as su -su.init_ee() - -@pytest.mark.skipif( - "EE_DECRYPT_KEY" in os.environ, reason="Don't work with Gservice account" -) class TestGee: - def test_wait_for_completion( - self, alert, fake_task, asset_description, asset_france - ): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_wait_for_completion(self, alert, fake_task, gee_dir, _hash): - res = gee.wait_for_completion(asset_description, alert) + # wait for the end of the the fake task + res = gee.wait_for_completion(fake_task, alert) assert res == "COMPLETED" assert alert.type == "success" assert alert.children[1].children[0] == ms.status.format("COMPLETED") # check that an error is raised when trying to overwrite a existing asset - description = "france" + description = "feature_collection" point = ee.FeatureCollection(ee.Geometry.Point([1.5, 1.5])) task_config = { "collection": point, - "description": description, - "assetId": asset_france, + "description": f"{description}_{_hash}", + "assetId": str(gee_dir / description), } task = ee.batch.Export.table.toAsset(**task_config) task.start() @@ -41,45 +34,54 @@ def test_wait_for_completion( return - def test_is_task(self, fake_task, asset_description): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_is_task(self, fake_task): # check if it exist - res = gee.is_task(asset_description) + res = gee.is_task(fake_task) assert res is not None return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_get_assets(self, gee_dir): # get the assets from the test repository list_ = gee.get_assets(gee_dir) # check that they are all there - names = ["corsica_template", "folder", "france", "imageViZExample", "italy"] + names = [ + "feature_collection", + "image", + "subfolder", + "subfolder/subfolder_feature_collection", + ] for item, name in zip(list_, names): - assert item["name"] == f"{gee_dir}/{name}" + assert item["name"] == str(gee_dir / name) return - def test_isAsset(self, gee_dir, asset_france): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_is_asset(self, gee_dir): # real asset - res = gee.is_asset(asset_france, gee_dir) + res = gee.is_asset(str(gee_dir / "image"), gee_dir) assert res is True # fake asset - res = gee.is_asset(f"{gee_dir}/toto", gee_dir) + res = gee.is_asset(str(gee_dir / "toto"), gee_dir) assert res is False return - def test_is_running(self, fake_task, asset_description): + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") + def test_is_running(self, fake_task): for _ in range(30): time.sleep(1) - res = gee.is_running(asset_description) + res = gee.is_running(fake_task) if res is not None: break @@ -88,22 +90,27 @@ def test_is_running(self, fake_task, asset_description): return @pytest.fixture - def fake_task(self, asset_id, asset_description, alert): + def fake_task(self, gee_dir, _hash, alert): + """create a fake exportation task""" - # create a fake exportation task + # init an asset point = ee.FeatureCollection(ee.Geometry.Point([1.5, 1.5])) + name = f"fake_collection_{_hash}" + asset_id = str(gee_dir / name) + + # launch the task task_config = { "collection": point, - "description": asset_description, + "description": name, "assetId": asset_id, } task = ee.batch.Export.table.toAsset(**task_config) task.start() - yield task + yield name # delete the task asset - gee.wait_for_completion(asset_description, alert) + gee.wait_for_completion(name, alert) ee.data.deleteAsset(asset_id) return diff --git a/tests/test_utils.py b/tests/test_utils.py index 34cc1d57..983ff9cb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,4 @@ import random -import warnings from configparser import ConfigParser from unittest.mock import patch @@ -100,6 +99,7 @@ def test_get_file_size(self): return + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_init_ee(self): # check that no error is raised @@ -107,93 +107,6 @@ def test_init_ee(self): return - def test_catch_errors(self): - - # create a fake object that uses the decorator - class Obj: - def __init__(self): - self.alert = sw.Alert() - self.btn = sw.Btn() - - self.func1 = su.catch_errors(alert=self.alert)(self.func) - self.func2 = su.catch_errors(alert=self.alert, debug=True)(self.func) - - def func(self, *args): - return 1 / 0 - - obj = Obj() - - obj.func1() - assert obj.alert.type == "error" - with pytest.raises(Exception): - obj.func2() - - return - - def test_loading_button(self): - - # create a fake object that uses the decorator - class Obj: - def __init__(self): - self.alert = sw.Alert() - self.btn = sw.Btn() - - @su.loading_button(debug=False) - def func1(self, *args): - return 1 / 0 - - @su.loading_button(debug=True) - def func2(self, *args): - return 1 / 0 - - @su.loading_button(debug=False) - def func3(self, *args): - warnings.warn("toto") - warnings.warn("sepal", SepalWarning) - return 1 - - @su.loading_button(debug=True) - def func4(self, *args): - warnings.warn("toto") - warnings.warn("sepal", SepalWarning) - return 1 - - obj = Obj() - - # should only display error in the alert - obj.func1(obj.btn, None, None) - assert obj.btn.disabled is False - assert obj.alert.type == "error" - - # should raise an error - obj.alert.reset() - with pytest.raises(Exception): - obj.fun2(obj.btn, None, None) - assert obj.btn.disabled is False - assert obj.alert.type == "error" - - # should only display the sepal warning - obj.alert.reset() - obj.func3(obj.btn, None, None) - assert obj.btn.disabled is False - assert obj.alert.type == "warning" - assert "sepal" in obj.alert.children[1].children[0] - assert "toto" not in obj.alert.children[1].children[0] - - # should raise warnings - obj.alert.reset() - with warnings.catch_warnings(record=True) as w_list: - obj.func4(obj.btn, None, None) - assert obj.btn.disabled is False - assert obj.alert.type == "warning" - assert "sepal" in obj.alert.children[1].children[0] - assert "toto" not in obj.alert.children[1].children[0] - msg_list = [w.message.args[0] for w in w_list] - assert any("sepal" in s for s in msg_list) - assert any("toto" in s for s in msg_list) - - return - def test_to_colors(self): # setup the same color in several formats @@ -215,70 +128,6 @@ def test_to_colors(self): return - def test_switch(self, capsys): - - # create a fake object that uses the decorator - class Obj: - def __init__(self): - self.valid = True - self.select = v.Select(disabled=False) - self.select2 = v.Select(disabled=False) - - # apply on non string - self.func4 = su.switch("disabled", on_widgets=[self.select])(self.func4) - - # apply the widget on the object itself - @su.switch("valid") - def func1(self, *args): - return True - - # apply the widget on members of the object - @su.switch("disabled", on_widgets=["select", "select2"]) - def func2(self, *args): - return True - - # apply it on a non existent widget - @su.switch("niet", on_widgets=["fake_widget"]) - def func3(self, *args): - return True - - def func4(self, *args): - return True - - # apply on a error func with debug = True - @su.switch("valid", debug=True) - def func5(self, *args): - return 1 / 0 - - # apply the switch with a non matching number of targets - @su.switch("disabled", on_widgets=["select", "select2"], targets=[True]) - def func6(self, *args): - return True - - obj = Obj() - - # assert - obj.func1() - assert obj.valid is True - - obj.func2() - assert obj.select.disabled is False - assert obj.select2.disabled is False - - with pytest.raises(Exception): - obj.func3() - - with pytest.raises(Exception): - obj.func4() - - with pytest.raises(Exception): - obj.func5() - - with pytest.raises(IndexError): - obj.func6() - - return - def test_next_string(self): # Arrange @@ -300,7 +149,7 @@ def test_set_config_locale(self): # create a config_file with a set language locale = "fr-FR" - su.set_config_locale(locale) + su.set_config("locale", locale) config = ConfigParser() config.read(config_file) @@ -309,7 +158,7 @@ def test_set_config_locale(self): # change an existing locale locale = "es-CO" - su.set_config_locale(locale) + su.set_config("locale", locale) config.read(config_file) assert config["sepal-ui"]["locale"] == locale @@ -326,7 +175,7 @@ def test_set_config_theme(self): # create a config_file with a set language theme = "dark" - su.set_config_theme(theme) + su.set_config("theme", theme) config = ConfigParser() config.read(config_file) @@ -335,7 +184,7 @@ def test_set_config_theme(self): # change an existing locale theme = "light" - su.set_config_theme(theme) + su.set_config("theme", theme) config.read(config_file) assert config["sepal-ui"]["theme"] == theme @@ -357,15 +206,15 @@ def test_set_style(self): return - @su.need_ee + @pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set") def test_geojson_to_ee(self): # create a point list points = [sg.Point(i, i + 1) for i in range(4)] d = {"col1": [str(i) for i in range(len(points))], "geometry": points} - gdf = gpd.GeoDataFrame(d, crs="EPSG:4326") - gdf_buffer = gdf.copy() - gdf_buffer.geometry = gdf_buffer.buffer(0.5) + gdf = gpd.GeoDataFrame(d, crs=4326) + gdf_buffer = gdf.copy().to_crs(3857) + gdf_buffer.geometry = gdf_buffer.buffer(500) # test a featurecollection ee_feature_collection = su.geojson_to_ee(gdf_buffer.__geo_interface__)