~tfardet/nngt-developers

NNGT: Maintenance commit v1 PROPOSED

CI moved to Ubuntu 22.04 LTS so build was adapted.
Changed some imports to keep ahead of some Python deprecations.
Changes for compatibility with Shapely 2.0
Updates for igraph 0.10 plots.
Modified geospatial countries due to international situation + pandas
deprecation.
Minor changes for matplotlib.

Tanguy Fardet (1):
  Maintenance: update CI, ubuntu, python + 3rd-party libs compatibility

 .build.yml                                   |   8 +-
 doc/examples/graph_structure/plot_layouts.py |  10 +-
 nngt/generation/cconnect.pyxbld              |   2 +-
 nngt/geometry                                |   2 +-
 nngt/geospatial/_cartopy_ne.py               | 158 -------------------
 nngt/geospatial/countries.py                 |  16 +-
 nngt/plot/chord_diag                         |   2 +-
 nngt/plot/custom_plt.py                      |   6 +-
 nngt/plot/plt_networks.py                    |  45 ++++--
 nngt/simulation/__init__.py                  | 140 +++++++---------
 setup.py                                     |   5 +-
 testing/test_examples.py                     | 117 +++++++-------
 testing/test_generation2.py                  |  42 ++---
 testing/test_io.py                           |  10 +-
 testing/test_plots.py                        |   5 +-
 15 files changed, 203 insertions(+), 365 deletions(-)
 delete mode 100644 nngt/geospatial/_cartopy_ne.py

-- 
2.34.4
#850948 .build.yml failed
NNGT/patches/.build.yml: FAILED in 2m33s

[Maintenance commit][0] from [~tfardet][1]

[0]: https://lists.sr.ht/~tfardet/nngt-developers/patches/35601
[1]: mailto:tanguyfardet@protonmail.com

✗ #850948 FAILED NNGT/patches/.build.yml https://builds.sr.ht/~tfardet/job/850948
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~tfardet/nngt-developers/patches/35601/mbox | git am -3
Learn more about email & git

[PATCH NNGT 1/1] Maintenance: update CI, ubuntu, python + 3rd-party libs compatibility Export this patch

From: Tanguy Fardet <tanguyfardet@protonmail.com>

---
 .build.yml                                   |   8 +-
 doc/examples/graph_structure/plot_layouts.py |  10 +-
 nngt/generation/cconnect.pyxbld              |   2 +-
 nngt/geometry                                |   2 +-
 nngt/geospatial/_cartopy_ne.py               | 158 -------------------
 nngt/geospatial/countries.py                 |  16 +-
 nngt/plot/chord_diag                         |   2 +-
 nngt/plot/custom_plt.py                      |   6 +-
 nngt/plot/plt_networks.py                    |  45 ++++--
 nngt/simulation/__init__.py                  | 140 +++++++---------
 setup.py                                     |   5 +-
 testing/test_examples.py                     | 117 +++++++-------
 testing/test_generation2.py                  |  42 ++---
 testing/test_io.py                           |  10 +-
 testing/test_plots.py                        |   5 +-
 15 files changed, 203 insertions(+), 365 deletions(-)
 delete mode 100644 nngt/geospatial/_cartopy_ne.py

diff --git a/.build.yml b/.build.yml
index f631aaf..d89ecb3 100644
--- a/.build.yml
+++ b/.build.yml
@@ -11,16 +11,16 @@ tasks:
        python3 extra/check_headers.py
    - setup: |
        sudo apt install -y software-properties-common
        sudo sh -c 'echo -n "deb https://downloads.skewed.de/apt focal main\n" >> /etc/apt/sources.list'
        sudo sh -c 'echo -n "deb https://downloads.skewed.de/apt jammy main\n" >> /etc/apt/sources.list'
        sudo apt-key adv --keyserver keys.openpgp.org --recv-key 612DEFB798507F25
        sudo add-apt-repository -y ppa:nest-simulator/nest
        sudo apt-get update -qq
        sudo apt install -y gcc libcairo2-dev pkg-config build-essential autoconf automake python3-dev libblas-dev libgeos-dev proj-bin libproj-dev
        sudo apt install -y nest liblapack-dev libatlas-base-dev gfortran libxml2-dev openmpi-bin libopenmpi-dev libgmp-dev
        sudo apt install -y python3-pip python3-tk libigraph0v5 libigraph0-dev python3-graph-tool python3-cairo python3-cairocffi
        sudo apt install -y python3-pip python3-tk libigraph-dev python3-graph-tool python3-cairo python3-cairocffi
        pip3 install numpy scipy cython mpi4py
        pip3 install networkx python-igraph
        pip3 install matplotlib seaborn shapely svg.path dxfgrabber 'cartopy<0.20' geopandas descartes
        pip3 install matplotlib seaborn shapely svg.path dxfgrabber cartopy geopandas descartes
        pip3 install pytest pytest-mpi cov-core coverage coveralls[yaml]
        cd NNGT
        python3 setup.py install --user
@@ -32,7 +32,7 @@ tasks:
        GL=nx coverage run -p -m pytest testing
        GL=ig coverage run -p -m pytest testing
        GL=nngt coverage run -p -m pytest testing
        GL=gt OMP=2 coverage run -p -m pytest -s testing
        GL=ig OMP=2 coverage run -p -m pytest -s testing
        GL=gt OMP=0 MPI=1 mpirun -n 2 coverage run -p -m pytest --with-mpi testing
        coverage combine
        GIT_BRANCH=$(git show -s --pretty=%D HEAD | tr -s ', /' '\n' | grep -v HEAD | sed -n 2p)
diff --git a/doc/examples/graph_structure/plot_layouts.py b/doc/examples/graph_structure/plot_layouts.py
index 07cbb45..c62e7bc 100644
--- a/doc/examples/graph_structure/plot_layouts.py
+++ b/doc/examples/graph_structure/plot_layouts.py
@@ -42,16 +42,22 @@ nngt.seed(0)
# set matplotlib backend depending on the library
mpl_backend = mpl.get_backend()

if nngt.get_config("backend") in ("graph-tool", "igraph"):
if nngt.get_config("backend") == "graph-tool":
    if mpl_backend.startswith("Qt4"):
        if mpl_backend != "Qt4Cairo":
            plt.switch_backend("Qt4Cairo")
    elif mpl_backend.startswith("Qt5"):
        if mpl_backend != "Qt5Cairo":
            plt.switch_backend("Qt5Cairo")
    elif mpl_backend.startswith("GTK"):
    elif mpl_backend.startswith("Qt"):
        if mpl_backend != "QtCairo":
            plt.switch_backend("QtCairo")
    elif mpl_backend.startswith("GTK3"):
        if mpl_backend != "GTK3Cairo":
            plt.switch_backend("GTK3Cairo")
    elif mpl_backend.startswith("GTK4"):
        if mpl_backend != "GTK4Cairo":
            plt.switch_backend("GTK4Cairo")
    else:
        plt.switch_backend("cairo")

diff --git a/nngt/generation/cconnect.pyxbld b/nngt/generation/cconnect.pyxbld
index 336b587..60988f0 100644
--- a/nngt/generation/cconnect.pyxbld
+++ b/nngt/generation/cconnect.pyxbld
@@ -1,5 +1,5 @@
import os
from distutils.extension import Extension
from setuptools import Extension
import numpy

dirname = os.path.dirname(__file__)
diff --git a/nngt/geometry b/nngt/geometry
index 3798616..b7fa825 160000
--- a/nngt/geometry
+++ b/nngt/geometry
@@ -1 +1 @@
Subproject commit 3798616de9d77b4f610a8bf01bfb4204d26f3e49
Subproject commit b7fa825d662f07cbf7dd4141806d994800cb22b2
diff --git a/nngt/geospatial/_cartopy_ne.py b/nngt/geospatial/_cartopy_ne.py
deleted file mode 100644
index bb33cda..0000000
--- a/nngt/geospatial/_cartopy_ne.py
@@ -1,158 +0,0 @@
#-*- coding:utf-8 -*-
#
# geospatial/_cartopy_ne.py
#
# This file is part of the NNGT project, a graph-library for standardized and
# and reproducible graph analysis: generate and analyze networks with your
# favorite graph library (graph-tool/igraph/networkx) on any platform, without
# any change to your code.
# Copyright (C) 2015-2022 Tanguy Fardet
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import io
import os

import cartopy
from cartopy.io import Downloader


"""
Temporary fix for Cartopy loading NaturalEarth data until the following PR
is merge: https://github.com/SciTools/cartopy/pull/1745

@todo: remove.
It is merged since Cartopy 0.19 but I'm waiting a bit to require it.
"""

class NEShpDownloader(Downloader):
    '''
    Specialise :class:`cartopy.io.Downloader` to download the zipped
    Natural Earth shapefiles and extract them to the defined location
    (typically user configurable).

    The keys which should be passed through when using the ``format_dict``
    are typically ``category``, ``resolution`` and ``name``.
    '''
    FORMAT_KEYS = ('config', 'resolution', 'category', 'name')

    # Define the NaturalEarth URL template. The natural earth website
    # returns a 302 status if accessing directly, so we use the naciscdn
    # URL directly.
    _NE_URL_TEMPLATE = ('https://naturalearth.s3.amazonaws.com/{resolution}'
                      '_{category}/ne_{resolution}_{name}.zip')

    def __init__(self, url_template=_NE_URL_TEMPLATE,
                 target_path_template=None, pre_downloaded_path_template=''):
        ''' adds some NE defaults to the __init__ of a Downloader'''
        super().__init__(url_template, target_path_template,
                         pre_downloaded_path_template)

    def zip_file_contents(self, format_dict):
        '''
        Return a generator of the filenames to be found in the downloaded
        natural earth zip file.

        '''
        for ext in ['.shp', '.dbf', '.shx', '.prj', '.cpg']:
            yield ('ne_{resolution}_{name}'
                   '{extension}'.format(extension=ext, **format_dict))

    def acquire_resource(self, target_path, format_dict):
        '''
        Download the zip file and extracts the files listed in
        :meth:`zip_file_contents` to the target path.

        '''
        from zipfile import ZipFile

        target_dir = os.path.dirname(target_path)
        if not os.path.isdir(target_dir):
            os.makedirs(target_dir)

        url = self.url(format_dict)

        shapefile_online = self._urlopen(url)

        zfh = ZipFile(io.BytesIO(shapefile_online.read()), 'r')

        for member_path in self.zip_file_contents(format_dict):
            ext = os.path.splitext(member_path)[1]
            target = os.path.splitext(target_path)[0] + ext
            member = zfh.getinfo(member_path.replace(os.sep, '/'))
            with open(target, 'wb') as fh:
                fh.write(zfh.open(member).read())

        shapefile_online.close()
        zfh.close()

        return target_path

    @staticmethod
    def default_downloader():
        '''
        Return a generic, standard, NEShpDownloader instance.

        Typically, a user will not need to call this staticmethod.

        To find the path template of the NEShpDownloader:

            >>> ne_dnldr = NEShpDownloader.default_downloader()
            >>> print(ne_dnldr.target_path_template)
            {config[data_dir]}/shapefiles/natural_earth/{category}/\
ne_{resolution}_{name}.shp
        '''
        default_spec = ('shapefiles', 'natural_earth', '{category}',
                        'ne_{resolution}_{name}.shp')
        ne_path_template = os.path.join('{config[data_dir]}', *default_spec)
        pre_path_template = os.path.join('{config[pre_existing_data_dir]}',
                                         *default_spec)
        return NEShpDownloader(target_path_template=ne_path_template,
                               pre_downloaded_path_template=pre_path_template)


# add a generic Natural Earth shapefile downloader to the cartopy config
# dictionary's 'downloaders' section.
_ne_key = ('shapefiles', 'natural_earth')
cartopy.config['downloaders'].setdefault(_ne_key,
                                         NEShpDownloader.default_downloader())


def natural_earth(resolution='110m', category='physical', name='coastline'):
    '''
    Return the path to the requested natural earth shapefile,
    downloading and unzipping if necessary.

    To identify valid components for this function, either browse
    NaturalEarthData.com, or if you know what you are looking for, go to
    https://github.com/nvkelso/natural-earth-vector/tree/master/zips to
    see the actual files which will be downloaded.

    Note
    ----
        Some of the Natural Earth shapefiles have special features which are
        described in the name. For example, the 110m resolution
        "admin_0_countries" data also has a sibling shapefile called
        "admin_0_countries_lakes" which excludes lakes in the country
        outlines. For details of what is available refer to the Natural Earth
        website, and look at the "download" link target to identify
        appropriate names.
    '''
    # get hold of the Downloader (typically a NEShpDownloader instance)
    # which we can then simply call its path method to get the appropriate
    # shapefile (it will download if necessary)
    ne_downloader = NEShpDownloader.default_downloader()
    format_dict = {'config': cartopy.config, 'category': category,
                   'name': name, 'resolution': resolution}
    return ne_downloader.path(format_dict)
diff --git a/nngt/geospatial/countries.py b/nngt/geospatial/countries.py
index 3a09984..4a8df21 100644
--- a/nngt/geospatial/countries.py
+++ b/nngt/geospatial/countries.py
@@ -23,12 +23,12 @@

import os

import numpy as np

import cartopy
import geopandas as gpd
import numpy as np
import pandas as pd

from ._cartopy_ne import natural_earth
from cartopy.io.shapereader import natural_earth
from shapely.geometry import Point


@@ -145,7 +145,8 @@ for i, v in world_50.iterrows():
                    len(new_countries) + size_adaptive
                ctn_adaptive[cval] = name

world = world_110.append(world_50.iloc[list(new_countries)], ignore_index=True)
world = pd.concat((world_110, world_50.iloc[list(new_countries)]),
                  ignore_index=True)

new_countries = []
size_adaptive = len(world)
@@ -171,7 +172,8 @@ for i, v in world_10.iterrows():

        # check if it's a subunit, if so, add only if it does not overlap
        # with sovereign territory (special case for Antigua and Barbuda)
        if v.LEVEL == 3 and sovc != "ATG":
        # ignore Israel unrecognized territories
        if v.LEVEL == 3 and sovc not in ("ATG", "IS1"):
            geom = v.geometry

            # get soverign territory
@@ -199,7 +201,7 @@ for i, v in world_10.iterrows():
        new_countries.append(i)


world = world.append(world_10.iloc[new_countries], ignore_index=True)
world = pd.concat((world, world_10.iloc[new_countries]), ignore_index=True)

maps["adaptive"] = world

@@ -300,7 +302,7 @@ for _, v in world.iterrows():
                continue

    points.append(v.geometry.representative_point())
        


country_points = gpd.GeoDataFrame({
    'country': world.NAME_LONG,
diff --git a/nngt/plot/chord_diag b/nngt/plot/chord_diag
index 97cdcaa..370fe8e 160000
--- a/nngt/plot/chord_diag
+++ b/nngt/plot/chord_diag
@@ -1 +1 @@
Subproject commit 97cdcaa26df25172c2729e310938eb27c6bf9799
Subproject commit 370fe8e80234950baaa1c655ac4ff18af284d16d
diff --git a/nngt/plot/custom_plt.py b/nngt/plot/custom_plt.py
index d74e94a..4f00a94 100755
--- a/nngt/plot/custom_plt.py
+++ b/nngt/plot/custom_plt.py
@@ -58,7 +58,7 @@ def palette_discrete(numbers=None):
        return pal(numbers)

# markers list
markers = [m for m in MS().filled_markers if m != '.']
markers = [m for m in MS.filled_markers if m != '.']

if nngt._config["color_lib"] == "seaborn":
    try:
@@ -122,9 +122,9 @@ def format_exponent(ax, axis='y', pos=(1.,0.), valign="top", halign="right"):
        ax_axis = ax.xaxis
    # Run plt.tight_layout() because otherwise the offset text doesn't update
    plt.tight_layout()
    ##### THIS IS A BUG 
    ##### THIS IS A BUG
    ##### Well, at least it's sub-optimal because you might not
    ##### want to use tight_layout(). If anyone has a better way of 
    ##### want to use tight_layout(). If anyone has a better way of
    ##### ensuring the offset text is updated appropriately
    ##### please comment!

diff --git a/nngt/plot/plt_networks.py b/nngt/plot/plt_networks.py
index 03e4b4b..b754766 100755
--- a/nngt/plot/plt_networks.py
+++ b/nngt/plot/plt_networks.py
@@ -23,6 +23,7 @@

from itertools import cycle
from collections import defaultdict
from pkg_resources import parse_version

import numpy as np

@@ -1236,11 +1237,13 @@ def library_draw(network, nsize="total-degree", ncolor=None, nshape="o",
    # backend and axis
    try:
        import igraph
        igv = igraph.__version__
        igv = parse_version(igraph.__version__)
    except:
        igv = '1.0'
        igv = parse_version('1.0')

    ig_test = nngt.get_config("backend") == "igraph" and igv <= '0.9.6'
    min_ig_version = parse_version('0.10.0')

    ig_test = nngt.get_config("backend") == "igraph" and igv < min_ig_version

    if nngt.get_config("backend") == "graph-tool" or ig_test:
        mpl_backend = mpl.get_backend()
@@ -1251,9 +1254,15 @@ def library_draw(network, nsize="total-degree", ncolor=None, nshape="o",
        elif mpl_backend.startswith("Qt5"):
            if mpl_backend != "Qt5Cairo":
                plt.switch_backend("Qt5Cairo")
        elif mpl_backend.startswith("GTK"):
        elif mpl_backend.startswith("Qt"):
            if mpl_backend != "QtCairo":
                plt.switch_backend("QtCairo")
        elif mpl_backend.startswith("GTK3"):
            if mpl_backend != "GTK3Cairo":
                plt.switch_backend("GTK3Cairo")
        elif mpl_backend.startswith("GTK4"):
            if mpl_backend != "GTK4Cairo":
                plt.switch_backend("GTK4Cairo")
        elif mpl_backend != "cairo":
            plt.switch_backend("cairo")

@@ -1311,7 +1320,7 @@ def library_draw(network, nsize="total-degree", ncolor=None, nshape="o",

    if nonstring_container(esize) and len(esize):
        esize *= max_esize / np.max(esize)
    

    # environment
    if spatial and network.is_spatial():
        if show_environment:
@@ -1426,9 +1435,9 @@ def library_draw(network, nsize="total-degree", ncolor=None, nshape="o",
            pos = nx.spring_layout(network.graph)

        # normalize sizes compared to igraph
        nsize = _increase_nx_size(nsize)
        nsize = _scale_node_size(nsize)

        nborder_width = _increase_nx_size(nborder_width, 2)
        nborder_width = _scale_node_size(nborder_width, 2)

        edges = None if restrict_edges is None else list(restrict_edges)

@@ -1474,6 +1483,10 @@ def library_draw(network, nsize="total-degree", ncolor=None, nshape="o",
        for k, v in convert_shape.items():
            shape_dict[k] = v

        if igv >= min_ig_version:
            # scale to normalize node size compared to other libraries
            nsize = _scale_node_size(nsize, factor=0.1)

        if nonstring_container(nsize):
            nsize = list(nsize)

@@ -1502,11 +1515,11 @@ def library_draw(network, nsize="total-degree", ncolor=None, nshape="o",
            eids  = [network.edge_id(e) for e in restrict_edges]
            graph = network.graph.subgraph_edges(eids, delete_vertices=False)

        # matplotlib interface is not working as of November 2021
        #  igraph.plot(graph, target=axis, **visual_style)

        graph_artist = GraphArtist(graph, axis, **visual_style)
        axis.artists.append(graph_artist)
        if igv >= min_ig_version:
            igraph.plot(graph, target=axis, **visual_style)
        else:
            graph_artist = GraphArtist(graph, axis, **visual_style)
            axis.add_artist(graph_artist)

    if "title" in kwargs:
        axis.set_title(kwargs["title"])
@@ -1942,7 +1955,7 @@ def _convert_to_nodes(node_restriction, name, network):
                ids.update(g.ids)
            return ids

        return set(node_restriction) 
        return set(node_restriction)
    elif isinstance(node_restriction, str):
        assert network.is_network(), \
            "`" + name + "` can be string only for Network."
@@ -2019,8 +2032,8 @@ def _to_ig_color(color):
    return color


def _increase_nx_size(size, factor=4):
    
def _scale_node_size(size, factor=4):
    ''' Multiply size by `factor` '''
    if isinstance(size, float) or is_integer(size):
        return factor*size
    elif nonstring_container(size) and len(size):
@@ -2033,7 +2046,7 @@ def _increase_nx_size(size, factor=4):
def _to_gt_prop(graph, value, cmap, ptype='node', color=False):
    pmap = (graph.new_vertex_property if ptype == 'node'
            else graph.new_edge_property)
    

    if nonstring_container(value) and len(value):
        if isinstance(value[0], str):
            if color:
diff --git a/nngt/simulation/__init__.py b/nngt/simulation/__init__.py
index 810f9f1..0f14f87 100755
--- a/nngt/simulation/__init__.py
+++ b/nngt/simulation/__init__.py
@@ -30,8 +30,9 @@ Module to interact easily with the NEST simulator. It allows to:
* plot the activity while separating the behaviours of predefined neural groups
"""

import sys as _sys
import logging as _logging
import sys as _sys
import types as _types

import nngt as _nngt
from nngt.lib.logger import _log_message
@@ -40,6 +41,63 @@ from nngt.lib.logger import _log_message
_logger = _logging.getLogger(__name__)


# --------- #
# Wrap nest #
# --------- #

import nest

warnlist = [
    "Connect", "Disconnect", "Create", "SetStatus", "get", "set",
    "ResetNetwork"
]

def _wrap_reset_kernel(func):
    '''
    Reset all NeuralPops and parent Networks before calling nest.ResetKernel.
    '''
    def wrapper(*args, **kwargs):
        _nngt.NeuralPop._nest_reset()

        return func(*args, **kwargs)

    return wrapper


def _wrap_warn(func):
    '''
    Warn when risky nest functions are called.
    '''
    def wrapper(*args, _warn=True, **kwargs):
        if _warn:
            _log_message(_logger, "WARNING", "This function could interfere "
                         "with NNGT, making your Network obsolete compared to "
                         "the one in NEST... make sure to check what is "
                         "modified!")

        return func(*args, **kwargs)

    return wrapper


class NestMod(_types.ModuleType):

    '''
    Wrapped module to replace nest.
    '''

    def __getattribute__(self, attr):
        if attr in warnlist:
            return _wrap_warn(getattr(nest, attr))
        elif attr == "ResetKernel":
            return _wrap_reset_kernel(nest.ResetKernel)

        return getattr(nest, attr)


_sys.modules["nest"] = NestMod("nest")


# -------------- #
# Import modules #
# -------------- #
@@ -72,83 +130,3 @@ except ImportError:
if _with_plot:
    from .nest_plot import plot_activity, raster_plot
    __all__.extend(("plot_activity", "raster_plot"))


# ---------------- #
# Wrap ResetKernel #
# ---------------- #

from nest import ResetKernel as _rk
from nest import Connect as _conn
from nest import Disconnect as _disc
from nest import Create as _cr
from nest import SetStatus as _setstat

try:
    from nest import ResetNetwork as _rn
except ImportError:
    pass

# store old functions
if not _nngt._old_nest_func:
    _nngt._old_nest_func["ResetKernel"] = _rk
    _nngt._old_nest_func["Connect"]     = _conn
    _nngt._old_nest_func["Disconnect"]  = _disc
    _nngt._old_nest_func["Create"]      = _cr
    _nngt._old_nest_func["SetStatus"]   = _setstat

    try:
        _nngt._old_nest_func["ResetNetwork"] = _rn
    except NameError:
        pass
else:
    _rk      = _nngt._old_nest_func["ResetKernel"]
    _conn    = _nngt._old_nest_func["Connect"]
    _disc    = _nngt._old_nest_func["Disconnect"]
    _cr      = _nngt._old_nest_func["Create"]
    _setstat = _nngt._old_nest_func["SetStatus"]

    try:
        _rn = _nngt._old_nest_func["ResetNetwork"]
    except KeyError:
        pass


def _new_reset_kernel():
    '''
    Call nest.ResetKernel, then reset all NeuralPops and parent Networks.
    '''
    _rk()
    _nngt.NeuralPop._nest_reset()


def _new_nest_func(old_nest_func):
    '''
    Print a warning to make sure user know what they are doing.
    '''
    def wrapper(*args, **kwargs):
        if kwargs.get("_warn", True):
            _log_message(_logger, "WARNING", "This function could interfere "
                         "with NNGT, making your Network obsolete compared to "
                         "the one in NEST... make sure to check what is "
                         "modified!")

        if "_warn" in kwargs:
            del kwargs["_warn"]

        return old_nest_func(*args, **kwargs)

    return wrapper


# nest is in sysmodules because it was imported in the main __init__.py
_sys.modules["nest"].ResetKernel = _new_reset_kernel
_sys.modules["nest"].Connect     = _new_nest_func(_conn)
_sys.modules["nest"].Disconnect  = _new_nest_func(_disc)
_sys.modules["nest"].Create      = _new_nest_func(_cr)
_sys.modules["nest"].SetStatus   = _new_nest_func(_setstat)

try:
    _sys.modules["nest"].ResetNetwork = _new_nest_func(_rn)
except NameError:
    pass
diff --git a/setup.py b/setup.py
index 73ffd17..50f3c96 100755
--- a/setup.py
+++ b/setup.py
@@ -166,9 +166,10 @@ setup_params = dict(
        'nx': ['networkx>=2.4'],
        'ig': ['python-igraph'],
        'geometry': ['matplotlib', 'shapely', 'dxfgrabber', 'svg.path'],
        'geospatial': ['matplotlib', 'geopandas', 'descartes', 'cartopy'],
        'geospatial': ['matplotlib', 'geopandas', 'descartes', 'cartopy>=0.19'],
        'full': ['networkx>=2.4', 'shapely', 'dxfgrabber', 'svg.path',
                 'matplotlib', 'geopandas', 'descartes', 'cartopy', 'lxml']
                 'matplotlib', 'geopandas', 'descartes', 'cartopy>=0.19',
                 'lxml']
    },

    # Cython module
diff --git a/testing/test_examples.py b/testing/test_examples.py
index fc497cb..5ba5d4b 100644
--- a/testing/test_examples.py
+++ b/testing/test_examples.py
@@ -12,39 +12,27 @@ Check that the examples work.

import os
from os import environ
from os.path import dirname, abspath, isfile, isdir, join
import unittest
from os.path import dirname, abspath, join

import pytest
from scipy.special import lambertw
import matplotlib as mpl

import nngt


''' Set state, paths, and global variables '''

with_plot, with_nest = None, None


def setup_module():
    ''' setup any state specific to the execution of the current module.'''
    with_plot = nngt.get_config("with_plot")
    with_nest = nngt.get_config("with_nest")

    nngt.set_config("with_plot", False)
    nngt.set_config("with_nest", False)


def teardown_module():
    ''' teardown any state that was previously setup with setup_module. '''
    nngt.set_config("with_plot", with_plot)
    nngt.set_config("with_nest", with_nest)
# prevent drawing
mpl.use("agg")

# set multithreading
nngt.set_config("omp", int(environ.get("OMP", 2)))

# set example dir
current_dir = dirname(abspath(__file__))
idx_testing = current_dir.find('testing')
example_dir = current_dir[:idx_testing] + 'doc/examples/'
example_dir = join(current_dir[:idx_testing], 'doc/examples/')

# set globals
glob = {"lambertw": lambertw}
@@ -55,52 +43,55 @@ glob = {"lambertw": lambertw}
# ---------- #

@pytest.mark.mpi_skip
class TestExamples(unittest.TestCase):

    '''
    Class testing saving and loading functions.
    '''

    example_files = []

    for f in os.listdir(example_dir):
        joint = join(example_dir, f)
        if joint.endswith(".py"):
            example_files.append(joint)
        elif isdir(joint):
            for f in os.listdir(joint):
                newjoint = join(joint, f)
                if newjoint.endswith(".py"):
                    example_files.append(newjoint)

    @classmethod
    def tearDownClass(cls):
        try:
            os.remove("sp_graph.el")
        except:
            pass

    @property
    def test_name(self):
        return "test_examples"

    @unittest.skipIf(int(environ.get("OMP", 1)) == 1, 'Check only with OMP')
    def test_examples(self):
        '''
        Test that the example files execute correctly.
        '''
        for example in self.example_files:
            if example.endswith('.py'):
                with open(example) as f:
                    code = compile(f.read(), example, 'exec')
                    exec(code, glob)
@pytest.mark.skipif(int(environ.get("OMP", 1)) == 1, reason='Run only with OMP')
def test_example_graph_struct():
    for root, _, files in os.walk(join(example_dir, "graph_structure")):
        for fname in files:
            if fname.endswith(".py"):
                fullname = join(root, fname)
                with open(fullname) as f:
                    code = compile(f.read(), fullname, 'exec')
                    try:
                        exec(code, {})
                    except Exception as e:
                        print(f"Running example file {fname} failed.")
                        raise e


# ---------- #
# Test suite #
# ---------- #
@pytest.mark.mpi_skip
@pytest.mark.skipif(int(environ.get("OMP", 1)) == 1, reason='Run only with OMP')
def test_example_graph_prop():
    for root, _, files in os.walk(join(example_dir, "graph_properties")):
        for fname in files:
            if fname.endswith(".py"):
                fullname = join(root, fname)
                with open(fullname) as f:
                    code = compile(f.read(), fullname, 'exec')
                    try:
                        exec(code)
                    except Exception as e:
                        print(f"Running example file {fname} failed.")
                        raise e

suite = unittest.TestLoader().loadTestsFromTestCase(TestExamples)

@pytest.mark.mpi_skip
@pytest.mark.skipif(int(environ.get("OMP", 1)) == 1, reason='Run only with OMP')
def test_examples():
    for root, _, files in os.walk(example_dir):
        if root == example_dir:
            for fname in files:
                if fname.endswith(".py"):
                    fullname = join(root, fname)
                    with open(fullname) as f:
                        code = compile(f.read(), fullname, 'exec')
                        try:
                            exec(code, glob)
                        except Exception as e:
                            print(f"Running example file {fname} failed.")
                            raise e

if __name__ == "__main__":
    unittest.main()
    if not nngt.get_config("mpi"):
        test_example_graph_struct()
        test_example_graph_prop()
        test_examples()
diff --git a/testing/test_generation2.py b/testing/test_generation2.py
index ad4a285..f0de03b 100644
--- a/testing/test_generation2.py
+++ b/testing/test_generation2.py
@@ -94,7 +94,7 @@ def test_newman_watts():

    for e in lattice_edges:
        assert g.has_edge(e)
    

    assert g.edge_nb() == 6  # min_edges + one shortcut

    # directed
@@ -339,37 +339,37 @@ def test_all_to_all():
def test_distances():
    ''' Check that distances are properly generated for SpatialGraphs '''
    # simple graph
    # ~ num_nodes = 4
    num_nodes = 4

    # ~ pos = [(0, 0), (1, 0), (2, 0), (3, 0)]
    
    # ~ g = nngt.SpatialGraph(num_nodes, positions=pos)
    pos = [(0, 0), (1, 0), (2, 0), (3, 0)]

    # ~ edges = [(0, 1), (0, 3), (1, 2), (2, 3)]
    g = nngt.SpatialGraph(num_nodes, positions=pos)

    # ~ g.new_edges(edges)
    edges = [(0, 1), (0, 3), (1, 2), (2, 3)]

    # ~ dist = g.edge_attributes["distance"]
    g.new_edges(edges)

    # ~ expected = np.abs(np.diff(g.edges_array, axis=1)).ravel()
    dist = g.edge_attributes["distance"]

    # ~ assert np.array_equal(dist, expected)
    expected = np.abs(np.diff(g.edges_array, axis=1)).ravel()

    # ~ g.new_node(positions=[(4, 0)])
    # ~ g.new_edge(1, 4)
    assert np.array_equal(dist, expected)

    # ~ assert g.get_edge_attributes((1, 4), "distance") == 3
    g.new_node(positions=[(4, 0)])
    g.new_edge(1, 4)

    # ~ # distance rule
    # ~ g = ng.distance_rule(2.5, rule="lin", nodes=num_nodes, avg_deg=2,
                         # ~ positions=pos)
    assert g.get_edge_attributes((1, 4), "distance") == 3

    # ~ dist = g.edge_attributes["distance"]
    # distance rule
    g = ng.distance_rule(2.5, rule="lin", nodes=num_nodes, avg_deg=2,
                         positions=pos)

    # ~ expected = np.abs(np.diff(g.edges_array, axis=1)).ravel()
    dist = g.edge_attributes["distance"]

    # ~ assert np.array_equal(dist, expected)
    # ~ assert np.all(dist < 3)
    expected = np.abs(np.diff(g.edges_array, axis=1)).ravel()

    assert np.array_equal(dist, expected)
    assert np.all(dist < 3)

    # using the connector functions
    num_nodes = 20
@@ -550,7 +550,7 @@ def test_sparse_clustered():
                if c*num_nodes > deg:
                    g = ng.sparse_clustered(
                        c, nodes=num_nodes, avg_deg=deg, connected=False,
                        directed=directed, rtol=0.09)
                        directed=directed, rtol=0.11)

                    g = ng.sparse_clustered(
                        c, nodes=num_nodes, avg_deg=deg, directed=directed,
diff --git a/testing/test_io.py b/testing/test_io.py
index 088339b..11e6eb8 100644
--- a/testing/test_io.py
+++ b/testing/test_io.py
@@ -72,7 +72,7 @@ class TestIO(TestBasis):
                os.remove(current_dir + 'test.' + ft)
        except:
            pass
    

    @property
    def test_name(self):
        return "test_io"
@@ -277,12 +277,14 @@ def test_spatial():

        h = nngt.load_from_file(gfilename, fmt=fmt)

        tolerance = np.average(np.abs(h.shape.exterior.coords))*1e-5

        assert np.all(np.isclose(g.get_positions(), h.get_positions()))
        assert g.shape.normalize().almost_equals(h.shape.normalize(), 1e-5)
        assert g.shape.normalize().equals_exact(h.shape.normalize(), tolerance)

        for name, area in g.shape.areas.items():
            assert area.normalize().almost_equals(
                h.shape.areas[name].normalize(), 1e-5)
            assert area.normalize().equals_exact(
                h.shape.areas[name].normalize(), tolerance)

            assert area.properties == h.shape.areas[name].properties

diff --git a/testing/test_plots.py b/testing/test_plots.py
index 080b08d..caed66d 100644
--- a/testing/test_plots.py
+++ b/testing/test_plots.py
@@ -17,6 +17,9 @@ import nngt.generation as ng
import nngt.plot as nplt


nngt.use_backend("igraph")


# absolute directory path

dirpath = os.path.abspath(os.path.dirname(__file__))
@@ -65,7 +68,7 @@ def test_draw_network_options():
    net = nngt.generation.erdos_renyi(nodes=100, avg_deg=10)

    net.set_weights(np.random.randint(0, 20, net.edge_nb()))
    

    net.new_node_attribute("attr", "int",
                           values=np.random.randint(-10, 20, 100))

-- 
2.34.4
NNGT/patches/.build.yml: FAILED in 2m33s

[Maintenance commit][0] from [~tfardet][1]

[0]: https://lists.sr.ht/~tfardet/nngt-developers/patches/35601
[1]: mailto:tanguyfardet@protonmail.com

✗ #850948 FAILED NNGT/patches/.build.yml https://builds.sr.ht/~tfardet/job/850948