diff --git a/talks/structuring/structuring.ipynb b/talks/structuring/structuring.ipynb index 5056719edaf6364e80de57161bd90e17a0a569ba..583cf16ece136223e698311c692138790098e422 100644 --- a/talks/structuring/structuring.ipynb +++ b/talks/structuring/structuring.ipynb @@ -13,8 +13,26 @@ "\n", "\n", "However, if you are intending to make your code available for others to use,\n", - "you will make their lives easier if you spend a little time organising your\n", - "project directory.\n", + "either as end users, or as a dependency of their own code, you will make their\n", + "lives much easier if you spend a little time organising your project\n", + "directory.\n", + "\n", + "\n", + "* [Recommended project structure](#recommended-project-structure)\n", + " * [The `mypackage/` directory](#the-mypackage-directory)\n", + " * [`README`](#readme)\n", + " * [`LICENSE`](#license)\n", + " * [`requirements.txt`](#requirements-txt)\n", + " * [`setup.py`](#setup-py)\n", + "* [Appendix: Tests](#appendix-tests)\n", + "* [Appendix: Versioning](#appendix-versioning)\n", + " * [Include the version in your code](#include-the-version-in-your-code)\n", + " * [Deprecate, don't remove!](#deprecate-dont-remove)\n", + "* [Appendix: Cookiecutter](#appendix-cookiecutter)\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"recommended-project-structure\"></a>\n", + "## Recommended project structure\n", "\n", "\n", "A Python project directory should, at the very least, have a structure that\n", @@ -33,7 +51,12 @@ "> ```\n", "\n", "\n", - "## The `mypackage/` directory\n", + "This example structure is in the `example_project/` sub-directory - have a\n", + "look through it if you like.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"the-mypackage-directory\"></a>\n", + "### The `mypackage/` directory\n", "\n", "\n", "The first thing you should do is make sure that all of your python code is\n", @@ -48,7 +71,8 @@ "more details on packages in Python.\n", "\n", "\n", - "## `README`\n", + "<a class=\"anchor\" id=\"readme\"></a>\n", + "### `README`\n", "\n", "\n", "Every project should have a README file. This is simply a plain text file\n", @@ -60,24 +84,126 @@ "(`README.md`).\n", "\n", "\n", - "## `LICENSE`\n", + "<a class=\"anchor\" id=\"license\"></a>\n", + "### `LICENSE`\n", "\n", "\n", "Having a LICENSE file makes it easy for people to understand the constraints\n", "under which your code can be used.\n", "\n", "\n", - "## `requirements.txt`\n", + "<a class=\"anchor\" id=\"requirements-txt\"></a>\n", + "### `requirements.txt`\n", "\n", "\n", "This file is not strictly necessary, but is very common in Python projects.\n", "It contains a list of the Python-based dependencies of your project, in a\n", - "standardised syntax.\n", + "standardised syntax. You can specify the exact version, or range of versions,\n", + "that your project requires. For example:\n", + "\n", + "\n", + "> six==1.*\n", + "> numpy==1.*\n", + "> scipy>=0.18,<2\n", + "> nibabel==2.*\n", + "\n", + "\n", + "If your project has optional dependencies, i.e. libraries which are not\n", + "critical but, if present, will allow your project to offer some extra\n", + "features, you can list them in a separate requirements file called, for\n", + "example, `requirements-extra.txt`.\n", + "\n", + "\n", + "Having all your dependencies listed in a file in this way makes it easy for\n", + "others to install the dependencies needed by your project, simply by running:\n", + "\n", + "\n", + "> pip install -r requirements.txt\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"setup-py\"></a>\n", + "### `setup.py`\n", + "\n", + "\n", + "This is the most important file (apart from your code, of course). Python\n", + "projects are installed using\n", + "[`setuptools`](https://setuptools.readthedocs.io/en/latest/), which is used\n", + "internally during both the creation of, and installation of Python libraries.\n", + "\n", + "\n", + "The `setup.py` file in a Python project is akin to a `Makefile` in a C/C++\n", + "project. But `setup.py` is also the location where you can define project\n", + "metadata (e.g. name, author, URL, etc) in a standardised format and, if\n", + "necessary, customise aspects of the build process for your library.\n", + "\n", + "\n", + "You generally don't need to worry about, or interact with `setuptools` at all.\n", + "With one exception - `setup.py` is a Python script, and its main job is to\n", + "call the `setuptools.setup` function, passing it information about your\n", + "project.\n", "\n", "\n", - "## Tests\n", + "The `setup.py` for our example project might look like this:\n", "\n", "\n", + "> ```\n", + "> #!/usr/bin/env python\n", + ">\n", + "> from setuptools import setup\n", + ">\n", + "> # Import version number from\n", + "> # the project package (see\n", + "> # the section on versioning).\n", + "> from mypackage import __version__\n", + ">\n", + "> # Read in requirements from\n", + "> # the requirements.txt file.\n", + "> with open('requirements.txt', 'rt') as f:\n", + "> requirements = [l.strip() for l in f.readlines()]\n", + ">\n", + "> setup(\n", + ">\n", + "> name='Example project',\n", + "> description='Example Python project for PyTreat',\n", + "> url='https://git.fmrib.ox.ac.uk/fsl/pytreat-2018-practicals/',\n", + "> author='Paul McCarthy',\n", + "> author_email='pauldmccarthy@gmail.com',\n", + "> license='Apache License Version 2.0',\n", + ">\n", + "> version=__version__,\n", + ">\n", + "> install_requires=requirements,\n", + ">\n", + "> classifiers=[\n", + "> 'Development Status :: 3 - Alpha',\n", + "> 'Intended Audience :: Developers',\n", + "> 'License :: OSI Approved :: Apache Software License',\n", + "> 'Programming Language :: Python :: 2.7',\n", + "> 'Programming Language :: Python :: 3.4',\n", + "> 'Programming Language :: Python :: 3.5',\n", + "> 'Programming Language :: Python :: 3.6',\n", + "> 'Topic :: Software Development :: Libraries :: Python Modules'],\n", + "> )\n", + "> ```\n", + "\n", + "\n", + "The `setup` function gets passed all of your project's metadata, including its\n", + "version number, depedencies, and licensing information. The `classifiers`\n", + "argument should contain a list of\n", + "[classifiers](https://pypi.python.org/pypi?%3Aaction=list_classifiers) which\n", + "are applicable to your project. Classifiers are purely for descriptive\n", + "purposes - they can be used to aid people in finding your project on\n", + "[`PyPI`](https://pypi.python.org/pypi), if you release it there.\n", + "\n", + "\n", + "See\n", + "[here](https://packaging.python.org/tutorials/distributing-packages/#setup-args)\n", + "for more details on `setup.py` and the `setup` function.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"appendix-tests\"></a>\n", + "## Appendix: Tests\n", + "\n", "\n", "There are no strict rules for where to put your tests (you have tests,\n", "right?). There are two main conventions:\n", @@ -113,17 +239,19 @@ "\n", "If you want your test code to be completely independent of your project's\n", "code, then go with the second option. However, if you would like your test\n", - "code to be distributed as part of your project (so that e.g. end users can run\n", + "code to be distributed as part of your project (e.g. so that end users can run\n", "them), then the first option is probably the best.\n", "\n", "\n", "But in the end, the standard Python unit testing frameworks\n", "([`pytest`](https://docs.pytest.org/en/latest/) and\n", "[`nose`](http://nose2.readthedocs.io/en/latest/)) are pretty good at finding\n", - "your test functions no matter where you've hidden them.\n", + "your test functions no matter where you've hidden them, so the choice is\n", + "really up to you.\n", "\n", "\n", - "## Versioning\n", + "<a class=\"anchor\" id=\"appendix-versioning\"></a>\n", + "## Appendix: Versioning\n", "\n", "\n", "If you are intending to make your project available for public use (e.g. on\n", @@ -179,14 +307,34 @@ "you can use to help manage your version number.\n", "\n", "\n", + "<a class=\"anchor\" id=\"include-the-version-in-your-code\"></a>\n", + "### Include the version in your code\n", + "\n", + "\n", + "While the version of a library is ultimately defined in `setup.py`, it is\n", + "standard practice for a Python library to contain a version string called\n", + "`__version__` in the `__init__.py` file of the top-level package. For example,\n", + "our `example_project/mypackage/__init__.py` file contains this line:\n", + "\n", + "\n", + "> __version__ = '0.1.0'\n", + "\n", + "\n", + "This makes a library's version number programmatically accessible and\n", + "queryable.\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"deprecate-dont-remove\"></a>\n", "### Deprecate, don't remove!\n", "\n", "\n", "If you really want to change your API, but can't bring yourself to increment\n", - "your major release number, consider _deprecating_ the old API, and postponing\n", - "its removal until you are ready for a major release. This will allow you to\n", - "change your API, but retain backwards-compatilbiity with the old API until it\n", - "can safely be removed at the next major release.\n", + "your major release number, consider\n", + "[_deprecating_](https://en.wikipedia.org/wiki/Deprecation#Software_deprecation)\n", + "the old API, and postponing its removal until you are ready for a major\n", + "release. This will allow you to change your API, but retain\n", + "backwards-compatilbiity with the old API until it can safely be removed at the\n", + "next major release.\n", "\n", "\n", "You can use the built-in\n", @@ -196,7 +344,8 @@ "it easy to mark a function, method or class as being deprecated.\n", "\n", "\n", - "## Cookiecutter\n", + "<a class=\"anchor\" id=\"appendix-cookiecutter\"></a>\n", + "## Appendix: Cookiecutter\n", "\n", "\n", "It is worth mentioning\n", @@ -215,26 +364,22 @@ "\n", "\n", "Here is how to create a skeleton project directory based off the minimal\n", - "Python packagetemplate:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pip install cookiecutter\n", + "Python packagetemplate:\n", "\n", - "# tell cookiecutter to create a directory\n", - "# from the pypackage-minimal template\n", - "cookiecutter https://github.com/kragniz/cookiecutter-pypackage-minimal.git\n", "\n", - "# cookiecutter will then prompt you for\n", - "# basic information (e.g. projectname,\n", - "# author name/email), and then create a\n", - "# new directory containing the project\n", - "# skeleton." + "> ```\n", + "> pip install cookiecutter\n", + ">\n", + "> # tell cookiecutter to create a directory\n", + "> # from the pypackage-minimal template\n", + "> cookiecutter https://github.com/kragniz/cookiecutter-pypackage-minimal.git\n", + ">\n", + "> # cookiecutter will then prompt you for\n", + "> # basic information (e.g. projectname,\n", + "> # author name/email), and then create a\n", + "> # new directory containing the project\n", + "> # skeleton.\n", + "> ```" ] } ], diff --git a/talks/structuring/structuring.md b/talks/structuring/structuring.md index 0eaee61f87ef9348faa36aba55d127edacf79c1e..efbac95e6c8a9b28fb7cc4d63d3b5d32d63ab794 100644 --- a/talks/structuring/structuring.md +++ b/talks/structuring/structuring.md @@ -7,8 +7,26 @@ can stop reading now. However, if you are intending to make your code available for others to use, -you will make their lives easier if you spend a little time organising your -project directory. +either as end users, or as a dependency of their own code, you will make their +lives much easier if you spend a little time organising your project +directory. + + +* [Recommended project structure](#recommended-project-structure) + * [The `mypackage/` directory](#the-mypackage-directory) + * [`README`](#readme) + * [`LICENSE`](#license) + * [`requirements.txt`](#requirements-txt) + * [`setup.py`](#setup-py) +* [Appendix: Tests](#appendix-tests) +* [Appendix: Versioning](#appendix-versioning) + * [Include the version in your code](#include-the-version-in-your-code) + * [Deprecate, don't remove!](#deprecate-dont-remove) +* [Appendix: Cookiecutter](#appendix-cookiecutter) + + +<a class="anchor" id="recommended-project-structure"></a> +## Recommended project structure A Python project directory should, at the very least, have a structure that @@ -27,7 +45,12 @@ resembles the following: > ``` -## The `mypackage/` directory +This example structure is in the `example_project/` sub-directory - have a +look through it if you like. + + +<a class="anchor" id="the-mypackage-directory"></a> +### The `mypackage/` directory The first thing you should do is make sure that all of your python code is @@ -42,7 +65,8 @@ Check out the `advanced_topics/02_modules_and_packages.ipynb` practical for more details on packages in Python. -## `README` +<a class="anchor" id="readme"></a> +### `README` Every project should have a README file. This is simply a plain text file @@ -54,23 +78,125 @@ for a README file to be written in plain text, (`README.md`). -## `LICENSE` +<a class="anchor" id="license"></a> +### `LICENSE` Having a LICENSE file makes it easy for people to understand the constraints under which your code can be used. -## `requirements.txt` +<a class="anchor" id="requirements-txt"></a> +### `requirements.txt` This file is not strictly necessary, but is very common in Python projects. It contains a list of the Python-based dependencies of your project, in a -standardised syntax. +standardised syntax. You can specify the exact version, or range of versions, +that your project requires. For example: + + +> six==1.* +> numpy==1.* +> scipy>=0.18,<2 +> nibabel==2.* + + +If your project has optional dependencies, i.e. libraries which are not +critical but, if present, will allow your project to offer some extra +features, you can list them in a separate requirements file called, for +example, `requirements-extra.txt`. + +Having all your dependencies listed in a file in this way makes it easy for +others to install the dependencies needed by your project, simply by running: -## Tests +> pip install -r requirements.txt + + +<a class="anchor" id="setup-py"></a> +### `setup.py` + + +This is the most important file (apart from your code, of course). Python +projects are installed using +[`setuptools`](https://setuptools.readthedocs.io/en/latest/), which is used +internally during both the creation of, and installation of Python libraries. + + +The `setup.py` file in a Python project is akin to a `Makefile` in a C/C++ +project. But `setup.py` is also the location where you can define project +metadata (e.g. name, author, URL, etc) in a standardised format and, if +necessary, customise aspects of the build process for your library. + + +You generally don't need to worry about, or interact with `setuptools` at all. +With one exception - `setup.py` is a Python script, and its main job is to +call the `setuptools.setup` function, passing it information about your +project. + + +The `setup.py` for our example project might look like this: + + +> ``` +> #!/usr/bin/env python +> +> from setuptools import setup +> +> # Import version number from +> # the project package (see +> # the section on versioning). +> from mypackage import __version__ +> +> # Read in requirements from +> # the requirements.txt file. +> with open('requirements.txt', 'rt') as f: +> requirements = [l.strip() for l in f.readlines()] +> +> setup( +> +> name='Example project', +> description='Example Python project for PyTreat', +> url='https://git.fmrib.ox.ac.uk/fsl/pytreat-2018-practicals/', +> author='Paul McCarthy', +> author_email='pauldmccarthy@gmail.com', +> license='Apache License Version 2.0', +> +> version=__version__, +> +> install_requires=requirements, +> +> classifiers=[ +> 'Development Status :: 3 - Alpha', +> 'Intended Audience :: Developers', +> 'License :: OSI Approved :: Apache Software License', +> 'Programming Language :: Python :: 2.7', +> 'Programming Language :: Python :: 3.4', +> 'Programming Language :: Python :: 3.5', +> 'Programming Language :: Python :: 3.6', +> 'Topic :: Software Development :: Libraries :: Python Modules'], +> ) +> ``` + + +The `setup` function gets passed all of your project's metadata, including its +version number, depedencies, and licensing information. The `classifiers` +argument should contain a list of +[classifiers](https://pypi.python.org/pypi?%3Aaction=list_classifiers) which +are applicable to your project. Classifiers are purely for descriptive +purposes - they can be used to aid people in finding your project on +[`PyPI`](https://pypi.python.org/pypi), if you release it there. + + +See +[here](https://packaging.python.org/tutorials/distributing-packages/#setup-args) +for more details on `setup.py` and the `setup` function. + + +<a class="anchor" id="appendix-tests"></a> +## Appendix: Tests There are no strict rules for where to put your tests (you have tests, @@ -107,17 +233,19 @@ Or, you can store your test files _alongside_ your package directory: If you want your test code to be completely independent of your project's code, then go with the second option. However, if you would like your test -code to be distributed as part of your project (so that e.g. end users can run +code to be distributed as part of your project (e.g. so that end users can run them), then the first option is probably the best. But in the end, the standard Python unit testing frameworks ([`pytest`](https://docs.pytest.org/en/latest/) and [`nose`](http://nose2.readthedocs.io/en/latest/)) are pretty good at finding -your test functions no matter where you've hidden them. +your test functions no matter where you've hidden them, so the choice is +really up to you. -## Versioning +<a class="anchor" id="appendix-versioning"></a> +## Appendix: Versioning If you are intending to make your project available for public use (e.g. on @@ -173,14 +301,34 @@ If you like to automate things, you can use to help manage your version number. +<a class="anchor" id="include-the-version-in-your-code"></a> +### Include the version in your code + + +While the version of a library is ultimately defined in `setup.py`, it is +standard practice for a Python library to contain a version string called +`__version__` in the `__init__.py` file of the top-level package. For example, +our `example_project/mypackage/__init__.py` file contains this line: + + +> __version__ = '0.1.0' + + +This makes a library's version number programmatically accessible and +queryable. + + +<a class="anchor" id="deprecate-dont-remove"></a> ### Deprecate, don't remove! If you really want to change your API, but can't bring yourself to increment -your major release number, consider _deprecating_ the old API, and postponing -its removal until you are ready for a major release. This will allow you to -change your API, but retain backwards-compatilbiity with the old API until it -can safely be removed at the next major release. +your major release number, consider +[_deprecating_](https://en.wikipedia.org/wiki/Deprecation#Software_deprecation) +the old API, and postponing its removal until you are ready for a major +release. This will allow you to change your API, but retain +backwards-compatilbiity with the old API until it can safely be removed at the +next major release. You can use the built-in @@ -190,7 +338,8 @@ module to warn about uses of deprecated items. There are also some it easy to mark a function, method or class as being deprecated. -## Cookiecutter +<a class="anchor" id="appendix-cookiecutter"></a> +## Appendix: Cookiecutter It is worth mentioning @@ -212,16 +361,16 @@ Here is how to create a skeleton project directory based off the minimal Python packagetemplate: -``` -pip install cookiecutter - -# tell cookiecutter to create a directory -# from the pypackage-minimal template -cookiecutter https://github.com/kragniz/cookiecutter-pypackage-minimal.git - -# cookiecutter will then prompt you for -# basic information (e.g. projectname, -# author name/email), and then create a -# new directory containing the project -# skeleton. -``` +> ``` +> pip install cookiecutter +> +> # tell cookiecutter to create a directory +> # from the pypackage-minimal template +> cookiecutter https://github.com/kragniz/cookiecutter-pypackage-minimal.git +> +> # cookiecutter will then prompt you for +> # basic information (e.g. projectname, +> # author name/email), and then create a +> # new directory containing the project +> # skeleton. +> ```