Skip to content

Commit

Permalink
Merge pull request #119 from Ouranosinc/explicit-logo-download
Browse files Browse the repository at this point in the history
Logo-handling class and remote fetching of Ouranos logos
  • Loading branch information
Zeitsperre authored Oct 17, 2023
2 parents b6b493f + 35f2fe8 commit e73be4d
Show file tree
Hide file tree
Showing 11 changed files with 863 additions and 190 deletions.
6 changes: 5 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ Contributors to this version: Sarah-Claude Bourdeau-Goulet (:user:`Sarahclaude`)
New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* New function hatchmap (:pull:`107`).
* Support for translating figures. Activating a locale through xclim's ``metadata_locales`` option will try to use metadata saved by xclim or xscen in this locale and to translate common terms appearing in the figures. Figanos currently ships with french translations of those terms. (:pull:`109`, :issue:`64`).
* Support for translating figures. Activating a locale through xclim's ``metadata_locales`` option will try to use metadata saved by `xclim` or xscen in this locale and to translate common terms appearing in the figures. Figanos currently ships with French translations of those terms. (:pull:`109`, :issue:`64`).
* New ``figanos.Logos`` class added to manage and install logos stored in user's Home configuration directory. The ``figanos.utils.plot_logo`` function call signature has changed to support the new system. (:issue:`115`, :pull:`119`).
* Logo sizing and placement now depends on `scikit-image` for resizing, and uses `width` and `height` instead of `zoom`. (:issue:`123`, :pull:`119`).
* Logo plotting now supports both PNG and SVG file types (via `cairosvg`). (:pull:`119`).

Bug fixes
^^^^^^^^^
Expand All @@ -19,6 +22,7 @@ Internal changes
^^^^^^^^^^^^^^^^
* Clean up of the dependencies to remove the notebooks deps from the core deps.
* `figanos` now uses Trusted Publishing to publish the package on PyPI and TestPyPI. (:pull:`113`).
* The official Ouranos logos have been removed from the repository. They can now be installed if required via the ``figanos.Logos.install_ouranos_logos`` class method. (:issue:`115`, :pull:`119`).

0.2.0 (2023-06-19)
------------------
Expand Down
615 changes: 454 additions & 161 deletions docs/notebooks/figanos_docs.ipynb

Large diffs are not rendered by default.

62 changes: 60 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,67 @@
Usage
=====

To use figanos in a project::
Quickstart
~~~~~~~~~~
To use figanos in a project:

.. code-block:: python
import figanos.matplotlib as fg
fg.utils.set_mpl_style('ouranos')
fg.utils.set_mpl_style("ouranos")
The style can be applied to any matplotlib figures, even if they are not created with figanos.

Logo Management
~~~~~~~~~~~~~~~
Figanos stores logos for convenience so that they can be called by name when creating figures. On installation, the `default` logo will be set to the `figanos_logo.png` file. Files are saved in the user's home configuration folder (`XDG_CONFIG_HOME` on Linux), in the `figanos/logos` folder.

For users who are permitted to use the Ouranos logos, they can be installed with the following command. You only need to run this once when setting up a new environment with figanos.

.. code-block:: python
from figanos import Logos
logos = Logos()
logos.default # Returns the path to the default logo
# '/home/username/.config/figanos/logos/figanos_logo.png'
logos.install_ouranos_logos(permitted=True)
# "Ouranos logos installed at /home/username/.config/figanos/logos"
logos.installed() # Returns the installed logo names
# ['default',
# 'figanos_logo',
# 'ouranos_logo_horizontal_blanc',
# 'ouranos_logo_horizontal_couleur',
# 'ouranos_logo_horizontal_noir',
# 'ouranos_logo_vertical_blanc',
# 'ouranos_logo_vertical_couleur',
# 'ouranos_logo_vertical_noir']
Custom Logos
^^^^^^^^^^^^
Custom logos can also be installed via the `figanos.Logos().set_logo()`` class method.

The ``set_logo()`` method takes the following arguments:

.. code-block:: python
from figanos import Logos
logos = Logos().set_logo(
"/path/to/my/file.png", # Path to the logo file
"my_custom_logo", # Name of the logo, optional
# If no name is provided, the name will be the file name without the extension
)
logos.my_custom_logo # Returns the installed logo path
To change the default to an already-installed logo, simply call the `set_logo()` method with the logo.<option> and the name set as `default`. For example:

.. code-block:: python
# To set the default logo to the horizontal white Ouranos logo
logos.set_logo(logos.ouranos_logo_horizontal_blanc, "default")
6 changes: 5 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ dependencies:
- python>=3.8
# Main
- cartopy
- cairosvg
- matplotlib-base
- seaborn
- geopandas
- numpy
- pandas
- platformdirs
- pyyaml
- scikit-image
- xarray
# To make the package and notebooks usable
- xclim >=0.38
# To make the package and notebooks usable
- dask
- h5py
- netcdf4
Expand Down
1 change: 1 addition & 0 deletions figanos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
__version__ = "0.2.0"

from . import matplotlib
from ._logo import Logos
210 changes: 210 additions & 0 deletions figanos/_logo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import logging
import shutil
import urllib.parse
import urllib.request
import warnings
from pathlib import Path
from typing import Optional, Union

import platformdirs
import yaml

__all__ = ["Logos"]

LOGO_CONFIG_FILE = "logo_mapping.yaml"
OURANOS_LOGOS_URL = "https://raw.githubusercontent.com/Ouranosinc/.github/main/images/"
_figanos_logo = Path(__file__).parent / "data" / "figanos_logo.png"


class Logos:
r"""Class for managing logos to be used in graphics.
Methods
-------
default
The path to the default logo.
can be temporarily set to a different logo via Logos().default = pathlib.Path().
installed()
Retrieves a list of installed logos.
install_ouranos_logos(\*, permitted: bool = False)
Fetches and installs the Ouranos logos.
set_logo(path: Union[str, Path], name: str = None)
Sets the path and name to a logo file.
If no logos are already set, the first one will be set as the default.
reload_config()
Reloads the logo configuration from the YAML file.
Examples
--------
>>> from figanos import Logos
>>> logos = Logos()
>>> logos.default
PosixPath('/home/user/.config/figanos/logos/figanos_logo.png')
>>> logos.installed()
['default', 'figanos_logo']
>>> logos.set_logo("path/to/logo.png", name="my_logo")
PosixPath('/home/user/.config/figanos/logos/my_logo.png')
>>> logos["my_logo"]
PosixPath('/home/user/.config/figanos/logos/my_logo.png')
>>> logos.default = "path/to/temporary/default/logo.png"
>>> logos
PosixPath("path/to/temporary/default/logo.png")
>>> logos.reload_config()
>>> logos.default
PosixPath('/home/user/.config/figanos/logos/figanos_logo.png')
"""

def __init__(self) -> None:
"""Constructor for the Logo class."""
self._config = None
self._catalogue = None
self._default = None
self._logos = {}
self.reload_config()

if not self._logos.get("default"):
warnings.warn(f"Setting default logo to {_figanos_logo}")
self.set_logo(_figanos_logo)
self.set_logo(_figanos_logo, name="default")

@property
def config(self) -> Path:
if self._config is None:
self._config = (
Path(platformdirs.user_config_dir("figanos", ensure_exists=True))
/ "logos"
)
return self._config

@property
def catalogue(self) -> Path:
if self._catalogue is None:
self._catalogue = self.config / LOGO_CONFIG_FILE
return self._catalogue

@property
def default(self) -> str:
"""The path to the default logo."""
return self._default

@default.setter
def default(self, value: Union[str, Path]):
"""Set a default logo."""
self._default = value

def _setup(self) -> None:
if (
not self.catalogue.exists()
or yaml.safe_load(self.catalogue.read_text()) is None
):
if not self.catalogue.exists():
warnings.warn(
f"No logo configuration file found. Creating one at {self.catalogue}."
)
self.config.mkdir(parents=True, exist_ok=True)
with open(self.catalogue, "w") as f:
yaml.dump(dict(logos={}), f)

def __str__(self) -> str:
return f"{self._default}"

def __repr__(self) -> str:
return f"{self._default}"

def __getitem__(self, args) -> Optional[str]:
"""Retrieve a logo filepath by its name.
If it does not exist, it will be installed, with the filepath returned.
"""
try:
return self._logos[args]
except (KeyError, TypeError):
if isinstance(args, tuple):
return self.set_logo(*args)
else:
return self.set_logo(args)

def reload_config(self) -> None:
"""Reload the configuration from the YAML file."""
self._setup()
self._logos = yaml.safe_load(self.catalogue.read_text())["logos"]
for logo_name, logo_path in self._logos.items():
if not Path(logo_path).exists():
warnings.warn(f"Logo file {logo_name} not found at {logo_path}.")
setattr(self, logo_name, logo_path)

def installed(self) -> list:
"""Retrieve a list of installed logos."""
return list(self._logos.keys())

def set_logo(
self, path: Union[str, Path], name: Optional[str] = None
) -> Optional[str]:
"""Copies the logo at a given path to the config folder and maps it to a given name in the logo config."""
_logo_mapping = yaml.safe_load(self.catalogue.read_text())["logos"]

logo_path = Path(path)
if logo_path.exists() and logo_path.is_file():
if name is None:
name = logo_path.stem
install_logo_path = self.config / logo_path.name

if not install_logo_path.exists():
shutil.copy(logo_path, install_logo_path)

logging.info("Setting %s logo to %s", name, install_logo_path)
_logo_mapping[name] = str(install_logo_path)
self.catalogue.write_text(yaml.dump(dict(logos=_logo_mapping)))
self.reload_config()
if name != "default":
return self._logos[name]
else:
return self._default

elif not logo_path.exists():
warnings.warn(f"Logo file `{logo_path}` not found. Not setting logo.")
elif not logo_path.is_file():
warnings.warn(f"Logo path `{logo_path}` is a folder. Not setting logo.")

def install_ouranos_logos(self, *, permitted: bool = False) -> None:
"""Fetches and installs the Ouranos logo.
The Ouranos logo is reserved for use by employees and project partners of Ouranos.
Parameters
----------
permitted : bool
Whether the user has permission to use the Ouranos logo.
"""
if permitted:
for orientation in ["horizontal", "vertical"]:
for colour in ["couleur", "blanc", "noir"]:
for suffix in ["png", "svg"]:
if suffix == "svg" and not (
orientation == "horizontal" and colour == "couleur"
):
continue
file = f"ouranos_logo_{orientation}_{colour}.{suffix}"
if not (self.config / file).exists():
logo_url = urllib.parse.urljoin(OURANOS_LOGOS_URL, file)
try:
urllib.request.urlretrieve(logo_url, self.config / file)
self.set_logo(self.config / file)
except Exception as e:
logging.error(
f"Error downloading or setting Ouranos logo: {e}"
)

if Path(self.default).stem == "figanos_logo":
_default_ouranos_logo = (
self.config / "ouranos_logo_horizontal_couleur.svg"
)
warnings.warn(f"Setting default logo to {_default_ouranos_logo}.")
self.set_logo(_default_ouranos_logo, name="default")
self.reload_config()
print(f"Ouranos logos installed at: {self.config}.")
else:
warnings.warn(
"You have not indicated that you have permission to use the Ouranos logo. "
"If you do, please set the `permitted` argument to `True`."
)
Binary file added figanos/data/figanos_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed figanos/data/ouranos_logo.png
Binary file not shown.
Binary file removed figanos/data/ouranos_logo_25.png
Binary file not shown.
Loading

0 comments on commit e73be4d

Please sign in to comment.