Repository: magpylib/magpylib (language: python)

Issue:
Rework CI workflows
We are currently using azure-pipelines and CircleCI to run our continuous integration jobs. When we included pyvista in the library it already required to adapt the workflows on both platforms. I would suggest to move to Github Workflows as unique CI tool. There is a nice piece of documentation specifically for python packages to get started:

https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

I will start working on it on a separate fork (not branch here) on my private repo to test it there independently of the magpylib repository settings.

@OrtnerMichael Could you help to fill out the list of things we want run through CI? I'll start to list them up here

- [x] unit test with pytest (docs and package)
  - [x] run matrix also on `[ubuntu-latest, macos-latest, windows-latest]`
  - [x] run matrix on `python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]`
  - [ ] architecture testing because of numerical stability ? (x64 or x86)
- [x] test coverage
- [ ] pre-commit hooks (code style, large files check, debug-statements, etc)
- [x] publish to Pypi including wheel (what about conda?)
- [ ] Code QL ? (already a GH action)



Discussion / hints:
1. do we need testing on x86? Do such systems still exists?
2. pre-commit hooks is up to you :) ... I'm not convinced we need them
3. Pypi publishing is part of GitHub workflows? I dont know about conda - but I guess the current config files will do?
4. CodeQL also up to you. Maybe we give it a try and if its annoying we dont do it? Its already implemented anyways.

1. what about pylint? (or is this replaced by CodeQL?)

Fix patch:
diff --git a/.binder/requirements.txt b/.binder/requirements.txt
index 30566431b..e9704b8eb 100644
--- a/.binder/requirements.txt
+++ b/.binder/requirements.txt
@@ -1,1 +1,1 @@
--r ../docs/requirements.txt
+.[docs]
diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index 7362ec50e..000000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,85 +0,0 @@
-version: 2.1
-workflows:
-  build_and_deploy:
-    jobs:
-      - build:
-          filters:
-            tags:
-              only: /.*/
-      - deploy:
-          requires:
-            - build
-          filters:
-            tags:
-              only: /.*/
-            branches:
-              ignore: /.*/
-
-jobs:
-  build:
-    docker:
-      - image: python:3.9
-    steps:
-      - checkout
-      - run:
-          name: Install local magpylib
-          command: pip install .[dev]
-      - run:
-          name: Set up testing tools and environment
-          command: mkdir test-results && pip install tox && pip install pylint
-      - run:
-          name: Specific dependencies for pyvista
-          command: |
-            apt update
-            apt-get install -y libgl1-mesa-dev xvfb ffmpeg
-      - run:
-          name: Run pylint test
-          command: pylint --rcfile='./.pylintrc' magpylib
-      - run:
-          name: Run code test coverage suite
-          command: tox
-    # The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass
-    resource_class: large
-
-  deploy:
-    docker:
-      - image: python:3
-    steps:
-      - checkout
-      - run:
-          name: install python dependencies
-          command: |
-            python3 -m venv venv
-            . venv/bin/activate
-            make dev
-      - run:
-          name: verify git tag vs. version
-          command: |
-            python3 -m venv venv
-            . venv/bin/activate
-            python setup.py verify
-      - run:
-          name: init .pypirc
-          command: |
-            echo -e "[pypi]" >> ~/.pypirc
-            echo -e "username: magpylib" >> ~/.pypirc
-            echo -e "password: $PYPI_PASSWORD" >> ~/.pypirc
-      - run:
-          name: create packages
-          command: |
-            make package
-      - run:
-          name: install wheel
-          command: |
-            . venv/bin/activate
-            pip install wheel
-      - run:
-          name: Create wheel
-          command: |
-            . venv/bin/activate
-            python setup.py bdist_wheel
-      - run:
-          name: upload to pypi
-          command: |
-            . venv/bin/activate
-            twine upload --repository pypi dist/*
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
new file mode 100644
index 000000000..842a08e13
--- /dev/null
+++ b/.github/workflows/python-app.yml
@@ -0,0 +1,79 @@
+name: Python package
+
+on: [push, pull_request]
+
+concurrency:
+  group: check-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.11"
+      - name: Set up testing tools and environment for pylint
+        run: |
+          python -m pip install --upgrade pip
+          pip install .[test]
+      - name: Run pylint test
+        run: pylint --rcfile='./.pylintrc' magpylib
+
+  test:
+    name: test with ${{ matrix.py }} on ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        py:
+          - "3.12"
+          - "3.11"
+          - "3.10"
+          - "3.9"
+          - "3.8"
+        os:
+          - ubuntu-latest
+          - macos-latest
+          - windows-latest
+    steps:
+      - name: Setup headless display
+        uses: pyvista/setup-headless-display-action@v2
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - name: Setup python for test ${{ matrix.py }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.py }}
+      - name: Install tox
+        run: python -m pip install tox-gh>=1.2
+      - name: Setup test suite
+        run: tox -vv --notest
+      - name: Run test suite
+        run: tox --skip-pkg-install
+
+  publish:
+    name: Publish to PyPi
+    needs: test
+    if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
+    runs-on: ubuntu-latest
+    permissions:
+      # IMPORTANT: this permission is mandatory for trusted publishing
+      id-token: write
+    steps:
+      - name: Checkout source
+        uses: actions/checkout@v4
+      - name: Set up Python 3.8
+        uses: actions/setup-python@v4
+        with:
+          python-version: 3.8
+      - name: Build package
+        run: |
+          pip install wheel
+          python setup.py sdist bdist_wheel
+      - name: Publish
+        uses: pypa/gh-action-pypi-publish@release/v1
+
diff --git a/.gitignore b/.gitignore
index 50e61f6e0..1577f408c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ share/python-wheels/
 .installed.cfg
 *.egg
 MANIFEST
+_autogen
 
 # docs
 docs/auto_examples
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 942d10dac..a9d46a073 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -29,5 +29,8 @@ sphinx:
 
 # Optionally declare the Python requirements required to build your docs
 python:
-   install:
-   - requirements: docs/requirements.txt
\ No newline at end of file
+  install:
+    - method: pip
+      path: .
+      extra_requirements:
+        - docs
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 0dc7f8824..000000000
--- a/Makefile
+++ /dev/null
@@ -1,23 +0,0 @@
-.PHONY: help clean dev docs package test
-
-help:
-	@echo "This project assumes that an active Python virtualenv is present."
-	@echo "The following make targets are available:"
-	@echo "	 dev 	install all deps for dev env"
-	@echo "  docs	create pydocs for all relveant modules"
-	@echo "	 test	run all tests with coverage"
-
-clean:
-	rm -rf dist/*
-
-dev:
-	pip install twine
-	pip install .
-	pip install setuptools
-
-package:
-	python setup.py sdist
-
-test:
-	coverage run -m pytest
-	coverage html
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index b2b68e80c..000000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-# Python package
-# Create and test a Python package on multiple Python versions.
-# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
-# https://docs.microsoft.com/azure/devops/pipelines/languages/python
-
-trigger:
-- main
-- feature-*
-
-pool:
-  vmImage: 'windows-latest'
-strategy:
-  matrix:
-    Python38:
-      python.version: '3.8'
-    Python39:
-      python.version: '3.9'
-    Python310:
-      python.version: '3.10'
-    Python311:
-      python.version: '3.11'
-steps:
-- task: UsePythonVersion@0
-  inputs:
-    versionSpec: '$(python.version)'
-  displayName: 'Use Python $(python.version)'
-
-- script: |
-    python -m pip install --upgrade pip
-    pip install .[dev]
-  displayName: 'Install dependencies'
-
-- script: |
-    pip install pytest pytest-azurepipelines
-    pytest
-  displayName: 'pytest'
diff --git a/docs/Makefile b/docs/Makefile
index c794a6b41..c88c67085 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -16,5 +16,5 @@ help:
 # Catch-all target: route all unknown targets to Sphinx using the new
 # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
 %: Makefile
-	@pip install -r ./requirements.txt
+	@pip install .[docs]
 	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 89e64019b..067ac0a64 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,41 +8,3 @@ The Documentation is built with [Sphinx](http://www.sphinx-doc.org/en/main/) v5.
 ### Handwritten documents
 Handwritten pages and guides are kept in the [_pages](./_pages) folder. They are all written in [Markdown](https://www.markdownguide.org/) using [myst-parser](https://github.com/executablebooks/MyST-Parser) as interface. Some documents like in the examples folder are dynamically computed with [myst-nb](https://github.com/executablebooks/myst-nb) as jupyter notebooks. With the help of the [jupytext](https://github.com/mwouts/jupytext) library ands its jupyterlab extension, examples can be written and executed within the jupyterlab ecosystem and saved as markdown file. It is recommended to use the [jupyterlab-myst](https://github.com/executablebooks/jupyterlab-myst) extension to be able to work with the full set of myst markdown flavor within jupyterlab. When editing the docs with vscode, use the [MyST-Markdown](https://marketplace.visualstudio.com/items?itemName=ExecutableBookProject.myst-highlight) extension to visualize the rendered document.
 
-
-### Building Locally
-
-This repository is set up to be easily built on [ReadTheDocs](https://readthedocs.org/) as the codebase is updated.
-
-##### To build locally on Linux,
-1. Install the dependencies on [requirements.txt](./requirements.txt):
-    ```
-    pip install requirements.txt
-    ```
-
-
-2. Run [make](http://man7.org/linux/man-pages/man1/make.1.html) to build the documentation:
-
-    ```bash
-
-    make html
-    ```
-
-This will create a `_build` folder with an `index.html`, containing the built documentation webpage structure.
-
----
-
-##### To build locally on Windows,
-
-1. [Install Sphinx](http://www.sphinx-doc.org/en/main/usage/installation.html)
-2. Install the dependencies on [requirements.txt](./requirements.txt):
-    ```
-    pip install -r requirements.txt
-    ```
-
-3. Build the documentation with the `.bat` script:
-
-    ```bash
-
-    make.bat html
-    ```
-
diff --git a/docs/requirements.txt b/docs/requirements.txt
deleted file mode 100644
index f3f9f72b2..000000000
--- a/docs/requirements.txt
+++ /dev/null
@@ -1,15 +0,0 @@
--e .
-pandas
-sphinx==5.3
-sphinx-copybutton
-sphinx-book-theme
-myst-nb
-sphinx-design
-sphinx-thebe
-jupytext
-numpy-stl
-pyvista==0.37
-panel
-jupyterlab-myst
-sphinx-favicon
-sphinx-gallery
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..26962d465
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,83 @@
+[build-system]
+requires = ["flit_core >=3.2,<4"]
+build-backend = "flit_core.buildapi"
+
+[project]
+name = "magpylib"
+dynamic = ["version"]
+dependencies = [
+    "numpy>=1.20",
+    "scipy>=1.7",
+    "matplotlib>=3.3",
+    "plotly>=5.3",
+    ]
+requires-python = ">=3.8"
+authors = [
+    {name = "Michael Ortner", email = "magpylib@gmail.com"},
+]
+maintainers = [
+    {name = "Alexandre Boisselet", email = "alexabois+magpylib@gmail.com"}
+]
+description = "Free Python3 package to compute magnetic fields."
+readme = "README.md"
+license = {file = "LICENSE"}
+keywords = ["magnetism", "physics", "analytical", "electromagnetic", "magnetic-field", "B-field"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Intended Audience :: Developers",
+    "Intended Audience :: Science/Research",
+    "Intended Audience :: Education",
+    "Operating System :: OS Independent",
+]
+
+[project.optional-dependencies]
+code_style = [
+    "pre-commit",
+]
+docs = [
+    "sphinx==5.3",
+    "sphinx-design",
+    "sphinx-thebe",
+    "sphinx-favicon",
+    "sphinx-gallery",
+    "sphinx-copybutton",
+    "sphinx-book-theme",
+    "myst-nb",
+    "pandas",
+    "numpy-stl",
+    "pyvista==0.37",
+    "panel",
+]
+test = [
+    "tox",
+    "pytest",
+    "pylint",
+    "coverage",
+    "pandas",
+    "pyvista",
+    "ipygany",
+    "imageio[tifffile]",
+    "kaleido",
+    "jupyterlab",
+]
+binder = [
+    "jupytext",
+    "jupyterlab>=3.2",
+    "jupyterlab-myst",
+]
+
+[project.urls]
+Documentation = "https://magpylib.readthedocs.io/en/latest/"
+Repository = "https://github.com/magpylib/magpylib.git"
+"Bug Tracker" = "https://github.com/magpylib/magpylib/issues"
+Changelog = "https://github.com/magpylib/magpylib/blob/master/CHANGELOG.md"
+
+
+[tool.pytest.ini_options]
+addopts = "--doctest-modules"
+testpaths = ["magpylib", "tests"]
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 145e9940f..000000000
--- a/setup.py
+++ /dev/null
@@ -1,112 +0,0 @@
-####
-# This is a basic setup.py structure so we can generate
-# distributable package information with setuptools.
-# More information: https://packaging.python.org/tutorials/packaging-projects/
-###
-###
-# Local install:
-#   Create virtual environment:
-#   $ conda create -n packCondaTest python=3.8.1 anaconda
-#   Activate:
-#   $ conda activate packCondaTest
-#   Generate distribution files (untracked by git):
-#   $ (packCondaTest) python3 setup.py sdist bdist_wheel
-#   Install the generated library for the environment:
-#   $ (packCondaTest) pip install .
-# The library is now in the packCondaTest environment.
-##
-import os
-import sys
-
-import setuptools
-from setuptools.command.install import install
-
-_magPyVersion = "4.4.1"
-_SphinxVersion = "5.3.0"
-_name = "magpylib"
-_description = "Free Python3 package to compute magnetic fields."
-_author_email = "magpylib@gmail.com"
-_author = "Michael Ortner"
-_projectUrl = "https://github.com/magpylib/magpylib"
-_release = "release"
-_license = "2-Clause BSD License, Simplified BSD License, FreeBSD License"
-
-with open("README.md") as fh:
-    long_description = fh.read()
-
-
-class VerifyVersionCommand(install):
-    """Custom command to verify that the git tag matches CircleCI version"""
-
-    description = "verify that the git tag matches CircleCI version"
-
-    def run(self):
-        tag = os.getenv("CIRCLE_TAG")
-
-        if tag != _magPyVersion:
-            info = f"Git tag: {tag} does not match the version of this app: {_magPyVersion}"
-            sys.exit(info)
-
-
-setuptools.setup(
-    name=_name,
-    version=_magPyVersion,
-    author=_author,
-    author_email=_author_email,
-    description=_description,
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    url=_projectUrl,
-    license=_license,
-    packages=setuptools.find_packages(),
-    zip_safe=False,  ## Gives the environment files so we can access docs,
-    ## enables tooltips but may decrease performance
-    install_requires=[
-        "numpy>=1.20",
-        "scipy>=1.7",
-        "matplotlib>=3.3",
-        "plotly>=5.3",
-    ],
-    # kaleido, jupyterlab are needed for testing with display(renderer='json', backend='plotly')
-    extras_require={
-        "dev": [
-            "kaleido",
-            "pytest",
-            "coverage",
-            "pylint",
-            "jupyterlab>=3.2",
-            "jupyterlab_myst",
-            "sphinx==5.3.0",
-            "pandas",
-            "pyvista",
-            "ipygany",
-            "imageio[tifffile]",
-        ]
-    },
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Intended Audience :: Developers",
-        "Intended Audience :: Science/Research",
-        "Intended Audience :: Education",
-        "License :: OSI Approved :: BSD License",
-        "Operating System :: OS Independent",
-    ],
-    python_requires="~=3.8",
-    keywords="magnetism physics analytical parallel electromagnetic fields b-field",
-    command_options={
-        "build_sphinx": {
-            "project": ("setup.py", _name),
-            "version": ("setup.py", _SphinxVersion),
-            "release": ("setup.py", _release),
-            "source_dir": ("setup.py", "./docs"),
-        }
-    },
-    cmdclass={
-        "verify": VerifyVersionCommand,
-    },
-)
diff --git a/tox.ini b/tox.ini
index ae357e9e1..e2efcccdd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,13 +1,30 @@
 [tox]
-envlist = py39
+min_version = 4.11.4
+env_list =
+    py311
+
+[gh]
+python =
+    3.12 = py312
+    3.11 = py311 #, type
+    3.10 = py310
+    3.9 = py39
+    3.8 = py38
 
 [testenv]
-# install pytest in the virtualenv where commands will be executed
-passenv = CODECOV_TOKEN
-deps = pytest-cov
-       codecov
-extras = dev
+description = run the tests with pytest
+package = wheel
+wheel_build_env = .pkg
+deps =
+    pytest>=7
+    pytest-sugar
+extras = test
+commands =
+    pytest {tty:--color=yes} {posargs}
+
+[testenv:docs]
+description = build documentation
+extras = docs
 commands =
-    # NOTE: you can run any command line tool here - not just tests
-    py.test --junitxml=test-results/magpylib/results.xml --cov-report html --cov-report term:skip-covered --cov=magpylib 
-    codecov
+  sphinx-build -d "{envtmpdir}{/}doctree" docs "{toxworkdir}{/}docs_out" --color -b html
+  python -c 'print(r"documentation available under file:\\{toxworkdir}{/}docs_out{/}index.html")'


Test patch:
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index e1267faf8..000000000
--- a/pytest.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-# content of pytest.ini
-[pytest]
-addopts = --doctest-modules
-testpaths = magpylib tests
\ No newline at end of file
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 9cfbb2c0e..01caa8d85 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -22,7 +22,7 @@ def getBHv_unknown_source_type():
         dimension=(0, 2, 1, 0, 360),
         position=(0, 0, -0.5),
         field="B",
-        **GETBH_KWARGS
+        **GETBH_KWARGS,
     )
 
 
@@ -131,7 +131,7 @@ def getBHv_bad_input1():
         field="B",
         magnetization=x2,
         dimension=x,
-        **GETBH_KWARGS
+        **GETBH_KWARGS,
     )
 
 
@@ -144,7 +144,7 @@ def getBHv_bad_input2():
         field="B",
         magnetization=x,
         dimension=x,
-        **GETBH_KWARGS
+        **GETBH_KWARGS,
     )
 
 
@@ -158,7 +158,7 @@ def getBHv_bad_input3():
         field="B",
         magnetization=x,
         dimension=x,
-        **GETBH_KWARGS
+        **GETBH_KWARGS,
     )