├── .github └── workflows │ └── pkg.yml ├── .gitignore ├── LICENSE ├── README.md ├── makepackage ├── __init__.py ├── __main__.py ├── makepackage.py ├── write_CLI_main.py ├── write_README.py ├── write_conftest.py ├── write_gitignore.py ├── write_license.py ├── write_module.py ├── write_module_init.py ├── write_pyproject.py ├── write_pytest_ini.py ├── write_setup.py ├── write_tests.py └── write_tests_init.py ├── pyproject.toml └── tests ├── conftest.py └── test_makepackage.py /.github/workflows/pkg.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with three versions of Python 2 | # and three operating systems. 3 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 4 | 5 | name: Python application 6 | 7 | on: 8 | push: 9 | branches: ["main", "dev"] 10 | pull_request: 11 | branches: ["main", "dev"] 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: [ubuntu-latest, windows-latest, macos-latest] 23 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v3 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | python -m pip install flake8 pytest pytest-cov setuptools wheel build 35 | - name: Build Package 36 | run: | 37 | python -m build 38 | - name: Install Built Package 39 | run: | 40 | pip install . 41 | - name: Lint with flake8 42 | run: | 43 | # stop the build if there are Python syntax errors or undefined names 44 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 45 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 46 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 47 | - name: Test with pytest 48 | run: | 49 | python -m pytest --cov=makepackage --cov-report=html tests/ 50 | # - name: Upload coverage reports to Codecov 51 | # uses: codecov/codecov-action@v3 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | pip-wheel-metadata/ 21 | share/python-wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .nox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | *.py,cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Environments 52 | .env 53 | .venv 54 | env/ 55 | venv/ 56 | ENV/ 57 | env.bak/ 58 | venv.bak/ 59 | venv_*/ 60 | venv-*/ 61 | 62 | # IDE config 63 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nyggus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `makepackage`: A Python package for packaging Python code 2 | 3 | ## Installation 4 | 5 | Install the package from [PyPi](https://pypi.org/project/makepackage/): 6 | 7 | ```shell 8 | $ pip install makepackage 9 | ``` 10 | 11 | 12 | ## TL;DR: How to use `makepackage` 13 | 14 | To create a package that does not need a command-line argument, go to a directory where you want to create the structure of your package and run in shell 15 | 16 | ```shell 17 | $ makepackage mypackage 18 | ``` 19 | 20 | where `mypackage` is the name of your package. That's it! You will have a self-standing package that you can now develop. Remember to fill in `[MAKEPACKAGE]` fields in pyproject.toml and LICENSE. 21 | 22 | If you want to create a package with a command-line argument, add a `cli` flag after the name of the package: 23 | 24 | ```shell 25 | $ makepackage mypackage cli 26 | ``` 27 | 28 | and here you are. Your package will get the command-line argument that is the same as the package's name. 29 | 30 | Now let's go into detail. 31 | 32 | 33 | ## Rationale and background 34 | 35 | Organizing Python code in a package has a lot of advantages and can significantly simplify development — but the first hours can be tricky. To facilitate this process, you can use tools such as [Cookiecutter](https://cookiecutter.readthedocs.io/), but they themselves are quite advanced and offer a lot of functionalities that you need to learn — quite often, this complexity makes development more difficult, sometimes changing the hours spent with the tool into a nightmare. 36 | 37 | To facilitate this step, I created a package template and have been using it for about a year. Life got easier. But the template required some manual work that could be automated. This is when I thought that a script would be better, so I wrote one. It worked fine indeed, and things got even easier. And then I thought, as this is so useful for me, why not make it useful for others? So, I made this package, and now you can use it just like me. 38 | 39 | `makepackage` offers only one function, available via shell. The only thing you need to do is to install `makepackage` (preferably in a virtual environment you will use only to create new packages) and run a simple shell command (which works under both Linux and Windows). The command, as you will see below, takes one required argument: a package's name; you can add `--cli` to create a CLI package, as otherwise (without the flag) it will not have command-line interface. 40 | 41 | The use of `makepackage` is very simple, but this does not come without costs: it creates just one type of structure, though you can change it manually: 42 | * you have to fill in some fields in pyproject.toml 43 | * pyproject.toml will include `pytest`, `wheel`, `black`, `mypy` and `setuptools` in the `dev` mode; you can remove them manually before installing the package in the editable mode 44 | * the package will use `pytest` for unit testing and `doctest` for documentation testing 45 | 46 | > You will find annotated code in `ziuziu` (given the simplicity of the functions, the annotations are very simple), and you can run `mypy` on it, with success. 47 | 48 | The idea behind `makepackage` is to offer a tool that creates a working package with a simple but common structure, which can be then extended and developed. And indeed, you will find in it tests (both `pytest`s and `doctest`s) that pass; you can install the package in the editable mode, and after that you will be able to import it. So, the resulting package is just fine, and you can immediately move to development. 49 | 50 | > `makepackage` offers one of many possible structures, and it assumes you will use `pytest` for testing. If you want to use other solutions, you should either create a package manually or use another tool. 51 | 52 | 53 | ## Using `makepackage` 54 | 55 | > The [tests](tests/) folder contains six shell scripts. Two of them show how to use `makepackage` in Linux, and two others do the same for Windows. One of the scripts shows how to create a package that does not need command-line interface while the other with CLI. Check out these two files for Linux: [`makepackage` without CLI](tests/run_makepackage_no_CLI.sh) and [`makepackage` with CLI](tests/run_makepackage_with_CLI.sh); and for Windows, these two files: [`makepackage` without CLI](tests/run_makepackage_no_CLI.bat) and [`makepackage` with CLI](tests/run_makepackage_with_CLI.bat). 56 | 57 | It's best to install and use `makepackage` in a virtual environment. So, for example, 58 | 59 | ```shell 60 | $ python -m venv venv-makepackage 61 | $ venv-package/bin/activate 62 | (venv-makepackage) $ python -m pip install makepackage 63 | ``` 64 | 65 | > Examples show Linux commands, but any Windows user will know how to replace them with the corresponding Windows commands (though most commands will be the same in Linux and Windows; you simply need to change paths when activating a virtual environment in Windows). 66 | 67 | Now that we have activated the virtual environment and installed `makepackage` in it, we are ready to create a package of our own. First, navigate to a folder where you want to create the package, and run the following command: 68 | 69 | ```shell 70 | (venv-makepackage) $ makepackage ziuziu 71 | ``` 72 | 73 | This creates a `ziuziu` package; `ziuziu` will not have command-line interface. You will now see a ziuziu folder: 74 | 75 | ```shell 76 | (venv-makepackage) $ ls 77 | ziuziu 78 | ``` 79 | 80 | If you want to create a package with command-line interface, use a command-line flag `--cli`, like this: 81 | 82 | ```shell 83 | (venv-makepackage) $ makepackage ziuziu --cli 84 | ``` 85 | 86 | > As we used the same name — `ziuziu` — again, we would get an error; so, you should first remove the previous installation of `ziuziu`, use a different name for the package, or create the package in a different location. 87 | 88 | With this, you will be able to run your package using the `ziuziu` command in shell. 89 | 90 | The only thing you now need to do is to create a virtual environment and install the `ziuziu` package there, in the editable mode: 91 | 92 | ```shell 93 | (venv-package) $ deactivate 94 | $ python -m pip install venv-ziuziu 95 | (venv-ziuziu) $ cd ziuziu 96 | (venv-ziuziu) $ python -m pip install -e .[dev] 97 | ``` 98 | 99 | And that's it, you're ready to develop `ziuziu`. Now you can run tests: 100 | 101 | ```shell 102 | (venv-ziuziu) $ python -m pytest 103 | (venv-ziuziu) $ python -m doctest ziuziu/ziuziu.py 104 | ``` 105 | 106 | You will see that the package is created with 11 `pytest` tests, and they should all pass (you will see the output from `pytest`). All `doctest`s should pass, too — that means you should see no output from `doctest`. 107 | 108 | 109 | > When you create a package using `makepackage`, you can read the README file of the new package. It contains some essential information about package development, such as building the package, installing it, and uploading to PyPi. 110 | 111 | ## Structure of a package created using `makepackage` 112 | 113 | You can use various structures to create a Python package. `makepackage` uses one of them, a simple (though not the simplest) but quite common one. You will see the following structure of the ziuziu/ folder (so, of the `ziuziu` package): 114 | 115 | ```shell 116 | . 117 | +-- .gitignore 118 | +-- LICENSE 119 | +-- README.md 120 | +-- pytest.ini 121 | +-- pyproject.toml 122 | +-- setup.py 123 | +-- tests 124 | | +-- __init__.py 125 | | +-- conftest.py 126 | | +-- test_ziuziu.py 127 | +-- ziuziu/ 128 | | +-- ziuziu.py 129 | | +-- __init__.py 130 | 131 | ``` 132 | 133 | When you used the `makepackage` command with the `cli` argument, the `ziuziu/ziuziu` folder will also include a `__main__.py` file. 134 | 135 | Here are the assumptions `makepackage` makes: 136 | * the package is developed using `pytest` and `doctest` (you will find both implemented in the code of `ziuziu`) 137 | * MIT license is used (you can change it to any license you want, but remember also to change the license in pyproject.toml) 138 | * in the development mode, `pytest`, `wheel`, `black` and `mypy` packages are additionally installed in the virtual environment (used for development); they are *not* installed when one installs the package from PyPi 139 | * you will need to fill in pyproject.toml in several places (namely, fields `authors`, `description`, `classifiers` and `project_urls`) and LICENSE in one place; you can easily find those places, as they are indicated with the `"[MAKEPACKAGE]"` mark. 140 | 141 | Of course, this is a starting point, and you can now extend the package however you want. Once installed, `ziuziu` (or however you name the package) works. It has three functions, `foo()`, `bar()` and `baz()`, which all have tests implemented in the tests/ folder, and you can run them using the `pytest` command as shown above. 142 | 143 | Those who tried to create such a package manually know that quite often something does not work — an import does not work, `pytest` does not see the package, and the like. When using `makepackage`, you get a fully working structure of the package. The only thing you need to do is to replace the existing functions with your functions, and of course to adapt the package to this change. 144 | 145 | 146 | > `makepackage` comes with some functionalities that you can get rid of: 147 | >> * a conftest.py file in the tests/ folder 148 | >> * simple annotations in the `foo()`, `bar()` and `baz()` functions of the newly created package 149 | >> * `doctest`s in the above functions 150 | >> * packages installed in the editable mode 151 | 152 | 153 | # Notes on further development of your package 154 | 155 | As mentioned before, the first step is to fill in several fields in pyproject.toml and author in LICENSE. Then you need to create a virtual environment, in which you install the package in the editable mode. And that's all you need to start development. 156 | 157 | From now on, you're on your own. However, a package created using `makepackage` comes with some help for inexperienced users. They can see how to write tests (using `pytest`), how to use a conftest.py file (for `pytest`ing), how to write fixtures and parametrized tests (again for `pytest`ing), how to import the package's modules and functions, how to write `doctest`s, and the like. These are just some basic development tools. 158 | 159 | There is one thing I'd like to stress, and it's related to imports. (The truth is, imports sometimes happen to pose some unexpected problems during Python coding). When you add a new module to the source folder (in our example, this is ziuziu/), e.g., ziuziu/another_ziuziu.py, then in the main `ziuziu` module you can import it as `from ziuziu import another_ziuziu` or `from ziuziu.another_ziuziu import another_foo`. Note that the regular approach you would use, that is, `import another_ziuziu`, will not work here. 160 | 161 | 162 | ## Testing 163 | 164 | Testing of `makepackage` combines shell scripts and `pytest`. Therefore, running tests on Linux and Windows requires running different shell scripts. You will learn more from [here](tests/README.md). 165 | 166 | 167 | ## Contribution 168 | 169 | Everyone is invited to develop `makepackage`. You can submit an issue or a pull request. Nonetheless, be aware that I will only accept proposals that 170 | * keep the current API of the package, unless the proposed change is so great that the cost of changing the API is relatively small compared to what the new functionality offers 171 | * are covered by unit tests 172 | * are well documented (if needed) 173 | * are coded in a similar style that the current code uses 174 | * work under both Windows and Linux 175 | 176 | Below, you can read more about these aspects. 177 | 178 | > Do remember to increment `makepackage`'s version. Use [semantic versioning 2.0.0](https://semver.org/). 179 | 180 | In technical terms, to contribute, 181 | * fork the repository and clone it to your machine 182 | * create a new branch: `$ git checkout -b new-branch`, where `new-branch` is the name of a branch, so remember to name it in a way that reflects what the branch changes (and please, do not use the name of `new-branch` or similar) 183 | * once you're done with all the changes and are ready to commit the changes, you can use `git add path` to add each file separately (`path` being a relative path to a file you want to commit); after each such command, do `$ git commit -m "What I did"`, the comment explaining what is changed in the committed file. If you want to add all the files at the same time, do `$ git add .`. 184 | * `$ git push --set-upstream origin new-branch` — this will create the branch in the repo and will push the changes to it. 185 | * create a pull request to the original repository; when doing so, please explain the changes in detail 186 | 187 | If someone else is developing `makepackage` at the same time, you may have to solve the resulting conflicts. How to say it... be patient and don't break down! Don't break down your computer, either! Keep your nerves in check! 188 | 189 | Now, you can sit and wait for a review of your proposal; use this time for thinking about how to improve the package even more :smiley:. 190 | 191 | 192 | #### Keep the current API of the package 193 | 194 | This, for instance, means that `makepackage`'s API does not offer different licences, structures of the root folder, and the like. Also, the API does not offer numerous arguments to enable the user to fill in the required fields of pyproject.toml; the user can do it directly in the file, an approach that is easier than providing this information through command-line arguments. No GUI, too: just a simple shell command. 195 | 196 | The simpler the API, the easier the package is to use. The idea behind `makepackage` was to bring a *really* simple API to create a package. This simplicity cannot come without cost, but the cost does not seem that great. True, if one wants to create a different organization of the package or wants to use `unittest` instead of `pytest`, then one will have to choose a different tool or do it manually. This is the main cost of simplicity we have to pay, but had `makepackage` enabled the user to choose from different options, the package's API would have been far more complicated. This would mean the main purpose behind creating the package — crreating the structure of a Python package in an easy way — would not have been accomplished. 197 | 198 | Simply put, `makepackage` has a simple API and does not offer too many choices, and I want to keep it that way. 199 | 200 | 201 | #### Cover all functionality by unit tests 202 | 203 | Add unit tests to every new functionality or change, unless the change does not change the package's functioning whatsoever. Remember that `makepackage` is tested with `pytest` and `doctest`. 204 | 205 | 206 | #### Use readable and sufficient documentation 207 | 208 | If you add a new functionality or change the existing one, you have to document it in documentation: README and docstrings. Of course, don't overdo, but note that this README is long and detailed. It has the [TL;DR: How to use makepackage](#tldr-how-to-use-makepackage) section, which is short and concise. Then, we go deep when explaining the details. Keep this approach. 209 | 210 | 211 | #### Maintain the current coding style 212 | 213 | This is important. Keep the current style, and please use `black` to format code. By coding style I do not only mean what `black` changes; I mean other important things, such as the following: 214 | 215 | * Have you noticed that the only classes that are defined in the package are those for custom exceptions? Try not to change that and do not base any new functionality on a class, unless this is a better and more natural approach. 216 | * `makepackage` uses custom exceptions to handle the user's mistakes. Throwing custom errors inside `makepackage` functions improves traceback, by using well-named exception classes and moving the traceback into the actual location in code where the exception occurred. 217 | 218 | 219 | #### Work under both Windows and Linux 220 | 221 | `makepackage` works in both these OSs, so if you want to propose something new, make sure this works under both these OSs. If you have problems with doing so, please contant the repo's maintainer. 222 | 223 | However, if you can check if `makepackage` works fine under a different OS, please do so and add it to this section. 224 | -------------------------------------------------------------------------------- /makepackage/__init__.py: -------------------------------------------------------------------------------- 1 | from .makepackage import ( 2 | NoPackageNameError, 3 | IncorrectCLIArgumentError, 4 | makepackage, 5 | ) 6 | -------------------------------------------------------------------------------- /makepackage/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections import namedtuple 3 | from easycheck import check_length, gt, lt 4 | from makepackage import ( 5 | NoPackageNameError, 6 | IncorrectCLIArgumentError, 7 | makepackage, 8 | ) 9 | 10 | 11 | def main(): 12 | package_name, CLI = _read_cli_args() 13 | makepackage(package_name, CLI) 14 | print_final_info(package_name) 15 | 16 | 17 | def _read_cli_args(): 18 | check_length( 19 | sys.argv, 20 | 1, 21 | NoPackageNameError, 22 | operator=gt, 23 | message="Provide package name as the first CLI argument", 24 | ) 25 | check_length( 26 | sys.argv, 27 | 4, 28 | IncorrectCLIArgumentError, 29 | operator=lt, 30 | message="Expected two CLI args: package name and (optionally) CLI", 31 | ) 32 | package_name = sys.argv[1] 33 | try: 34 | if sys.argv[2].lower() in ("--cli", "-cli", "cli"): 35 | CLI = True 36 | else: 37 | CLI = False 38 | except IndexError: 39 | CLI = False 40 | print( 41 | f"Creating package <{package_name}>, " 42 | f"with{'' if CLI else 'out'} command-line interface." 43 | ) 44 | return namedtuple("Settings", "package_name CLI")(package_name, CLI) 45 | 46 | 47 | def print_final_info(package_name: str) -> None: 48 | print( 49 | "\n" 50 | f"Package <{package_name}> has been created." 51 | "\nTo finish, you need to fill in the following fields in setup.py:\n" 52 | " - author\n" 53 | " - author_email\n" 54 | " - description (this is a short description, as the long one is taken from README)\n" 55 | "You need to also fill in the author in LICENSE. You can find all those fields" 56 | "by searching for '[MAKEPACKAGE]' in the project.\n" 57 | "When you're done, the package is ready to be develop.\n" 58 | "Check if you need all libraries from extras_require in setup.py - they will " 59 | "be installed in the development mode of the package (not after installing from " 60 | "the wheel file)." 61 | ) 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /makepackage/makepackage.py: -------------------------------------------------------------------------------- 1 | import os 2 | from easycheck import check_if_not 3 | from pathlib import Path 4 | 5 | from makepackage.write_README import write_README 6 | from makepackage.write_setup import write_setup 7 | from makepackage.write_pyproject import write_pyproject 8 | from makepackage.write_license import write_license 9 | from makepackage.write_gitignore import write_gitignore 10 | from makepackage.write_CLI_main import write_CLI_main 11 | 12 | from makepackage.write_module_init import write_module_init 13 | from makepackage.write_module import write_module 14 | 15 | from makepackage.write_pytest_ini import write_pytest_ini 16 | from makepackage.write_conftest import write_conftest 17 | from makepackage.write_tests import write_tests 18 | from makepackage.write_tests_init import write_tests_init 19 | 20 | 21 | class NoPackageNameError(Exception): 22 | pass 23 | 24 | 25 | class IncorrectCLIArgumentError(Exception): 26 | pass 27 | 28 | 29 | class FolderExistsError(Exception): 30 | pass 31 | 32 | 33 | def makepackage(package_name: str, CLI: bool) -> None: 34 | """Create a package package_name, with or without CLI.""" 35 | 36 | # create directories 37 | root_path = (Path(".") / f"{package_name}").absolute() 38 | make_dirs(root_path, package_name) 39 | 40 | # write files in the root folder 41 | write_setup(root_path, package_name, CLI) 42 | write_pyproject(root_path, package_name, CLI) 43 | write_README(root_path, package_name, CLI) 44 | write_pytest_ini(root_path) 45 | write_license(root_path) 46 | write_gitignore(root_path) 47 | 48 | # write files in the module's folder 49 | module_path = root_path / f"{package_name}" 50 | write_module(module_path, package_name) 51 | write_module_init(module_path, package_name) 52 | 53 | # write files in the tests/ folder 54 | tests_path = root_path / "tests" 55 | write_conftest(tests_path) 56 | write_tests_init(tests_path) 57 | write_tests(tests_path, package_name) 58 | 59 | # write __main__, if CLI is True 60 | if CLI: 61 | write_CLI_main(module_path, package_name) 62 | 63 | 64 | def make_dirs(root_path: Path, package_name: str) -> None: 65 | """Create the directory structure.""" 66 | check_if_not( 67 | root_path.exists(), 68 | FolderExistsError, 69 | message=f"Folder {root_path} already exists.", 70 | ) 71 | os.mkdir(str(root_path)) 72 | for dir in { 73 | "tests", 74 | package_name, 75 | }: 76 | this_path = root_path / dir 77 | check_if_not( 78 | this_path.exists(), 79 | FolderExistsError, 80 | message=f"Folder {dir} already exists.", 81 | ) 82 | os.mkdir(str(this_path)) 83 | -------------------------------------------------------------------------------- /makepackage/write_CLI_main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_CLI_main(path: Path, package_name: str) -> None: 5 | CLI_main = f"""from {package_name} import foo, bar, baz 6 | 7 | 8 | def main(): 9 | print("Running all {package_name} functions via" 10 | " the command-line command ($ {package_name}): " 11 | ) 12 | print( 13 | "This is foo(): ", foo(1), 14 | "Function bar() uses str.lower() method:", bar("Lower"), 15 | "Function baz() uses str.upper() method:", baz("Upper") 16 | ) 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | """ 22 | with open(path / "__main__.py", "w") as f: 23 | f.write(CLI_main) 24 | -------------------------------------------------------------------------------- /makepackage/write_README.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_README(path: Path, package_name: str, CLI: bool) -> None: 5 | README = f""" 6 | # `{package_name}`: A Python package for ... 7 | 8 | # Installation - development 9 | 10 | Create a virtual environment, for example using `venv`: 11 | 12 | ```shell 13 | $ python -m venv venv-{package_name} 14 | $ source venv-{package_name}/bin/activate 15 | $ mkdir {package_name} 16 | $ cd {package_name} 17 | $ python -m pip install -e .[dev] 18 | 19 | ``` 20 | 21 | Note that the last command installs a development environment, as it also 22 | installs packages needed for development, like `black` (for code formatting), 23 | `pytest` (for unit testing) and `wheel` (for creating wheel files from the package). 24 | 25 | # Testing 26 | 27 | Running the tests requires to run the following command in the root folder 28 | (of course in the virtual environment): 29 | 30 | ```shell 31 | (venv-{package_name}) > pytest 32 | ``` 33 | 34 | If you use doctests in your docstrings (as `makepackage` assumes), you can run them 35 | using the following command (in the root folder): 36 | 37 | ```shell 38 | (venv-{package_name}) > python -m doctest {package_name}/{package_name}.py 39 | ``` 40 | 41 | In a similar way, you can run doctests from any other file that contains doctests. 42 | 43 | ## Versioning 44 | 45 | Remember to update package version once a change is made to the package and the new 46 | version is pushed to the repository. Don't forget about releases, too. 47 | 48 | ## How to build a Python package? 49 | 50 | To build the package, you need to go to the root folder of the package and run the 51 | following command: 52 | 53 | ```shell 54 | (venv-{package_name}) > python -m build . 55 | ``` 56 | 57 | Note that this assumes you have `wheel` installed in your virtual environment, and 58 | `makepackage` does this for you. 59 | 60 | The built package is now located in the dist/ folder. 61 | 62 | ## Publishing your package in PyPi 63 | 64 | If you want to publish it to [PyPi](https://pypi.org/), you need to install 65 | [twine](https://twine.readthedocs.io/en/latest/), create an account there, and run 66 | the following command (also in the package's root folder): 67 | 68 | ```shell 69 | (venv-{package_name}) > twine upload dist/* 70 | ``` 71 | 72 | Nonetheless, if you first want to check what it will look like in PyPi, you can first 73 | upload the package to [a test version of PyPi](https://test.pypi.org/), that is, 74 | 75 | ```shell 76 | twine upload -r testpypi dist/* 77 | ``` 78 | 79 | Check if everything is fine, and if so, you're ready to publish the package to PyPi. 80 | 81 | ## Installation from PyPi 82 | 83 | If the package is in PyPi, you can install it from there like any other Python package, 84 | that is, 85 | 86 | ```shell 87 | pip install {package_name} 88 | ``` 89 | """ 90 | if CLI: 91 | README += f""" 92 | 93 | # CLI app 94 | 95 | `{package_name}` offers a command-line interface, which you can run from shell using 96 | the following command: 97 | 98 | ```shell 99 | $ {package_name} 100 | ``` 101 | """ 102 | 103 | with open(path / "README.md", "w") as f: 104 | f.write(README) 105 | -------------------------------------------------------------------------------- /makepackage/write_conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_conftest(path: Path) -> None: 5 | with open(path / "conftest.py", "w") as f: 6 | f.write( 7 | """import pytest 8 | 9 | from typing import Tuple 10 | 11 | @pytest.fixture 12 | def strings() -> Tuple[str, str, str]: 13 | return ( 14 | "Whatever string", 15 | "Shout Bamalama!", 16 | "Sing a song about statistical models.", 17 | ) 18 | 19 | """ 20 | ) 21 | -------------------------------------------------------------------------------- /makepackage/write_gitignore.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | gitignore = """# Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # Distribution / packaging 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Environments 54 | .env 55 | .venv 56 | env/ 57 | venv/ 58 | ENV/ 59 | env.bak/ 60 | venv.bak/ 61 | venv_*/ 62 | venv-*/ 63 | """ 64 | 65 | 66 | def write_gitignore(path: Path) -> None: 67 | with open(path / ".gitignore", "w") as f: 68 | f.write(gitignore) 69 | -------------------------------------------------------------------------------- /makepackage/write_license.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from pathlib import Path 3 | 4 | 5 | def write_license(path: Path) -> None: 6 | LICENSE = f"""MIT License 7 | 8 | Copyright (c) {date.today().year} [MAKEPACKAGE] 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | """ 28 | with open(path / "LICENSE", "w") as f: 29 | f.write(LICENSE) 30 | -------------------------------------------------------------------------------- /makepackage/write_module.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | module = '''def foo(x: int) -> int: 4 | """Calculate square of x. 5 | 6 | >>> foo(2) 7 | 4 8 | >>> foo(100) 9 | 10000 10 | """ 11 | return x**2 12 | 13 | 14 | def bar(string: str) -> str: 15 | """Use method .lower() to a string. 16 | 17 | Args: 18 | string (str): a string to be manipulated 19 | 20 | Returns: 21 | str: a manipulated string 22 | 23 | >>> bar("Whatever!") 24 | 'whatever!' 25 | >>> bar("AAaa...!") 26 | 'aaaa...!' 27 | """ 28 | return string.lower() 29 | 30 | 31 | def baz(string: str) -> str: 32 | """Use method .upper() to a string. 33 | 34 | Args: 35 | string (str): a string to be manipulated 36 | 37 | Returns: 38 | str: a manipulated string 39 | 40 | >>> baz("Whatever!") 41 | 'WHATEVER!' 42 | >>> baz("AAaa...!") 43 | 'AAAA...!' 44 | """ 45 | return string.upper() 46 | ''' 47 | 48 | 49 | def write_module(path: Path, package_name: str) -> None: 50 | with open(path / f"{package_name}.py", "w") as f: 51 | f.write(module) 52 | -------------------------------------------------------------------------------- /makepackage/write_module_init.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_module_init(path: Path, package_name: str) -> None: 5 | module_init = f"""from .{package_name} import foo, bar, baz 6 | """ 7 | with open(path / "__init__.py", "w") as f: 8 | f.write(module_init) 9 | -------------------------------------------------------------------------------- /makepackage/write_pyproject.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | from pathlib import Path 3 | 4 | 5 | def write_pyproject(path: Path, package_name: str, CLI: bool) -> None: 6 | config = configparser.ConfigParser() 7 | 8 | # Add sections to the ConfigParser object 9 | config["build-system"] = { 10 | "requires": ["setuptools>=61.0"], 11 | "build-backend": '"setuptools.build_meta"', 12 | } 13 | config["project"] = { 14 | "name": f"'{package_name}'", 15 | "version": '"0.1.0"', 16 | "authors": '[{name = "[MAKEPACKAGE]", email = "yourname@example.com"},]', 17 | "description": '"[MAKEPACKAGE]"', 18 | "readme": '"README.md"', 19 | "license": '{file = "LICENSE"}', 20 | "requires-python": '">=3.8"', 21 | "dependencies": [], 22 | "classifiers": [ 23 | "Programming Language :: Python :: 3", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | ], 27 | } 28 | config["project.urls"] = {"Homepage": '"https://example.com"'} 29 | config["tool.setuptools"] = {"packages": [f"{package_name}"]} 30 | 31 | if CLI: 32 | config["project.scripts"] = { 33 | f"{package_name}": f'"{package_name}.__main__:main"' 34 | } 35 | 36 | config["project.optional-dependencies"] = { 37 | "dev": ["wheel", "black", "pytest", "mypy", "setuptools"] 38 | } 39 | 40 | with open(path / "pyproject.toml", "w") as f: 41 | config.write(f) 42 | -------------------------------------------------------------------------------- /makepackage/write_pytest_ini.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_pytest_ini(path: Path) -> None: 5 | with open(path / "pytest.ini", "w") as f: 6 | f.write( 7 | """[pytest] 8 | testpaths = tests 9 | """ 10 | ) 11 | -------------------------------------------------------------------------------- /makepackage/write_setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_setup(path: Path, package_name: str, CLI: bool) -> None: 5 | setup = """from setuptools import setup\n\nsetup()\n""" 6 | 7 | with open(path / "setup.py", "w") as f: 8 | f.write(setup) 9 | -------------------------------------------------------------------------------- /makepackage/write_tests.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_tests(path: Path, package_name: str) -> None: 5 | with open(path / f"test_{package_name}.py", "w") as f: 6 | f.write( 7 | f"""import pytest 8 | from {package_name}.{package_name} import foo, bar, baz 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "test_input,expected", 13 | [(2, 4), (3, 9), (100, 10000), (1.2, 1.44), (-1, 1), (-2, 4), (-50, 2500), 14 | ]) 15 | def test_foo(test_input, expected): 16 | assert foo(test_input) == expected 17 | 18 | 19 | def test_bar_basic(): 20 | assert bar("AAAaA") == "aaaaa" 21 | 22 | 23 | def test_bar(strings): 24 | for string in strings: 25 | assert not any(s.isupper() for s in bar(string)) 26 | 27 | 28 | def test_baz_basic(): 29 | assert baz("AAAaA") == "AAAAA" 30 | 31 | 32 | def test_baz(strings): 33 | for string in strings: 34 | assert not any(s.islower() for s in baz(string)) 35 | 36 | """ 37 | ) 38 | -------------------------------------------------------------------------------- /makepackage/write_tests_init.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write_tests_init(path: Path) -> None: 5 | with open(path / "__init__.py", "w") as f: 6 | f.write("") 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "makepackage" 7 | version = "0.2.2" 8 | authors = [ 9 | { name = "Nyggus", email = "nyggus@gmail.com" }, 10 | { name = "Patrick Boateng", email = "boatengpato.pb@gmail.com" }, 11 | ] 12 | description = "Creating a structure of a simple Python package" 13 | readme = "README.md" 14 | license = { file = "LICENSE" } 15 | requires-python = ">=3.6" 16 | dependencies = ["easycheck"] 17 | classifiers = [ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | ] 22 | 23 | [project.urls] 24 | Homepage = "https://github.com/nyggus/makepackage/" 25 | 26 | [tool.setuptools] 27 | packages = ["makepackage"] 28 | 29 | [project.scripts] 30 | makepackage = "makepackage.__main__:main" 31 | 32 | [project.optional-dependencies] 33 | dev = ["wheel", "black", "pytest", "mypy", "setuptools", "build"] 34 | 35 | [tool.black] 36 | line-length = 79 37 | 38 | [pytest] 39 | testpaths = ["tests"] 40 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from typing import Dict, List 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def py_cmd() -> str: 9 | return "python" if platform.system() == "Windows" else "python3" 10 | 11 | 12 | @pytest.fixture 13 | def files_no_CLI() -> Dict[str, List[str]]: 14 | return { 15 | "root": [ 16 | "README.md", 17 | ".gitignore", 18 | "pytest.ini", 19 | "setup.py", 20 | "pyproject.toml", 21 | "LICENSE", 22 | ], 23 | "src": ["__init__.py", "pkgNoCLI.py"], 24 | "test": ["__init__.py", "conftest.py", "test_pkgNoCLI.py"], 25 | } 26 | 27 | 28 | @pytest.fixture 29 | def files_with_CLI() -> Dict[str, List[str]]: 30 | return { 31 | "root": [ 32 | "README.md", 33 | ".gitignore", 34 | "pytest.ini", 35 | "setup.py", 36 | "pyproject.toml", 37 | "LICENSE", 38 | ], 39 | "src": ["__init__.py", "__main__.py", "pkgWithCLI.py"], 40 | "test": ["__init__.py", "conftest.py", "test_pkgWithCLI.py"], 41 | } 42 | -------------------------------------------------------------------------------- /tests/test_makepackage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import platform 4 | import subprocess 5 | from pathlib import Path 6 | from typing import Dict, List, Tuple 7 | 8 | cmd_command = Tuple[str, Path] 9 | 10 | 11 | def select_venv_cmd(): 12 | if platform.system() == "Windows": 13 | return r".\.venv\Scripts\activate" 14 | 15 | return "source .venv/bin/activate" 16 | 17 | 18 | def run_cmds(cmds: List[cmd_command]): 19 | if platform.system() == "Windows": 20 | executable = None 21 | else: 22 | executable = "/bin/bash" 23 | 24 | for cmd, path in cmds: 25 | process = subprocess.Popen( 26 | args=cmd, 27 | shell=True, 28 | executable=executable, 29 | cwd=path, 30 | stdout=subprocess.PIPE, 31 | stderr=subprocess.PIPE, 32 | ) 33 | 34 | _, stderr = process.communicate() 35 | 36 | if process.returncode != 0: 37 | raise RuntimeError( 38 | f"Command failed with error code {process.returncode}: {stderr.decode()}" 39 | ) 40 | 41 | 42 | def test_pkg_no_CLI( 43 | tmp_path: Path, 44 | py_cmd: str, 45 | files_no_CLI: Dict[str, List[str]], 46 | ): 47 | pkg_name = "pkgNoCLI" 48 | pkg_path = tmp_path / pkg_name 49 | src_dir = tmp_path / pkg_name / pkg_name 50 | tests_dir = tmp_path / pkg_name / "tests" 51 | venv_command = select_venv_cmd() 52 | 53 | commands = [ 54 | (f"makepackage {pkg_name}", tmp_path), 55 | ( 56 | f"{py_cmd} -m venv .venv && {venv_command} && pip install -e .[dev]", 57 | pkg_path, 58 | ), 59 | ("pytest", pkg_path), 60 | (f"{py_cmd} -m doctest {src_dir / pkg_name}.py", pkg_path), 61 | ] 62 | 63 | run_cmds(commands) 64 | 65 | files = files_no_CLI 66 | 67 | assert (pkg_path / ".venv").exists() 68 | assert (pkg_path).exists() 69 | assert (src_dir).exists() 70 | assert (tests_dir).exists() 71 | 72 | assert all((pkg_path / file).exists() for file in files["root"]) 73 | assert all((src_dir / file).exists() for file in files["src"]) 74 | assert all((tests_dir / file).exists() for file in files["test"]) 75 | 76 | 77 | def test_pkg_with_CLI( 78 | tmp_path: Path, 79 | py_cmd: str, 80 | files_with_CLI: Dict[str, List[str]], 81 | ): 82 | pkg_name = "pkgWithCLI" 83 | pkg_path = tmp_path / pkg_name 84 | src_dir = tmp_path / pkg_name / pkg_name 85 | tests_dir = tmp_path / pkg_name / "tests" 86 | venv_command = select_venv_cmd() 87 | 88 | commands = [ 89 | (f"makepackage {pkg_name} --cli", tmp_path), 90 | ( 91 | f"{py_cmd} -m venv .venv && {venv_command} && pip install -e .[dev]", 92 | pkg_path, 93 | ), 94 | ("pytest", pkg_path), 95 | (f"{py_cmd} -m doctest {src_dir / pkg_name}.py", pkg_path), 96 | ] 97 | 98 | run_cmds(commands) 99 | 100 | files = files_with_CLI 101 | 102 | assert (pkg_path / ".venv").exists() 103 | assert (pkg_path).exists() 104 | assert (src_dir).exists() 105 | assert (tests_dir).exists() 106 | 107 | assert all((pkg_path / file).exists() for file in files["root"]) 108 | assert all((src_dir / file).exists() for file in files["src"]) 109 | assert all((tests_dir / file).exists() for file in files["test"]) 110 | --------------------------------------------------------------------------------