/ PYTHONENV.md
PYTHONENV.md
1 # Notes on using emacs as a Python 'IDE': eply or lsp-mode 2 3 There are two Python IDE environments I have used, and which this configuration 4 can support: *lsp-mode* with *python-lsp-server* or *elpy*. 5 6 Both provide advanced Python specific IDE abilities and greatly enhance the 7 Python programming experience inside *emacs*. 8 9 Both have some of the same features and this overlap means they are not fully 10 compatible. You will have to pick between them. 11 12 In general, *elpy* is a bit leaner. *lsp-mode* is slower and provides a more 13 consistent interface between the different programming languages which 14 *lsp-mode* supports. 15 16 *lsp-mode* is a compelling choice if you use the lsp-server to enforce 17 consistent style and code requirements across a shop/team. You simply pick a 18 server and configure it with the standard requirements and everyone can use 19 their own preferred editor/IDE pointed at the configured lsp server. Then 20 everyone is writing code to the same standards. 21 22 The default setup for Python uses *python-lsp-server*. 23 24 ## Using *python-lsp-server* 25 26 *lsp-mode* should start or connect to an existing *python-lsp-server* whenever 27 you open any python file. 28 29 If lsp-mode is running poorly, you can triage the situation with *lsp-doctor*. 30 31 A good guide for using *lsp-doctor* and increasing *lsp-mode* performance is 32 found here: <https://emacs-lsp.github.io/lsp-mode/page/performance/>. 33 34 A good guide for configuring and setting up *python-lsp-server* is here: 35 <https://emacs-lsp.github.io/lsp-mode/page/lsp-pylsp/#server> 36 37 38 ## Enabling and using elpy 39 40 See: <https://elpy.readthedocs.io/en/latest/introduction.html> for more info on *elpy*. 41 42 By default, *elpy* is disabled in favor of LSP, but you can edit 43 *custom/setup-python.el* to enable it. First, uncomment sections referencing 44 *elpy*. and then comment all sections in all files referencing *lsp-mode* and 45 python in *custom/setup-editing.el*. 46 47 To make sure elpy is running correctly, *M-X elpy-config*. Note any *warnings* 48 or missing packages and install them. 49 50 If you are using a dedicated virtual environment per project, elpy may warn 51 about *~/.local/bin* not being in the PATH. You will have to either: modify your PATH 52 variable before starting emacs, or stop running emacs from the virtual 53 environment. 54 55 To modify your path, add a line like the following to your *~/.bashrc*: 56 57 ```bash 58 export PATH="$PATH:$HOME/.local/bin" 59 ``` 60 61 ## Adding and configuring additional Python packages: autopep8, yapf, jedi, rope, black, flake8 62 63 These pip packages provide features used by *elpy* and/or *python-lsp-server*, 64 but are handy on their own. 65 66 * *autopep8* helps elpy enforce compliance with the Python Pep-8 coding standards 67 by underlining in red all non-compliant code. See: 68 <http://paetzke.me/project/py-autopep8.el>. 69 70 * *flake8* is the linter static analysis engine used by autopep8. 71 72 * *yapf* is used as a more feature completed *autopep8* with a slightly different 73 and more hands-on approach to enforcing code-style beyond pep8 compliance. 74 75 In general, you should configure your editor to use 1 of these at a time. 76 77 However, you can still install all of them on your machine and have different 78 linting tools run them as part of your gating process-- both inside or outside 79 of *emacs*. 80 81 82 To install all python packages supported by Elpy: 83 84 ```bash 85 pip install autopep8 jedi rope yapf black flake8 pydocstyle 86 ``` 87 88 To install all optional packages supported by *python-lsp-server*: 89 90 ```bash 91 pip install python-lsp-server[all] 92 93 ``` 94 95 Sometimes, you may want to override some of autopep8's style settings, such as 96 the default line length being set to only allow 80 characters, which is archaic. 97 98 Another issue is that pycodestyle enforces W503 and W504 out of the box 99 when all warnings are enabled. These two rules actually conflict with each 100 other. The current best practice in PEP8 is to use W504, as W503 is deprecated. 101 102 To change settings, edit *$HOME/.config/pycodestyle* and add the appropriate 103 ini-file style configuration settings. The following changes the default line 104 length from 80 to 160 characters and turns off W503: 105 106 ``` 107 [pycodestyle] 108 max_line_length = 160 109 ignore = W503 110 ``` 111 112 To override the default pylint settings, create an rc file in your home 113 directory or project root called *.pylintrc*: 114 115 ``` 116 [MASTER] 117 118 persistent=yes 119 120 [FORMAT] 121 max-line-length=160 122 123 ``` 124 125 To override settings in flake8, look for file *~/.config/flake8*: 126 ``` 127 [flake8] 128 max-line-length = 160 129 ``` 130 131 ## Using the python invoke package 132 133 Most of my Python projects use a package called *invoke* to execute the creation 134 of build and release targets. *invoke* is a "task execution tool and library." 135 It is similar in purpose to *make*, *maven* or Ruby *rake*. 136 137 You can install it via *pip*: 138 139 ```bash 140 pip install invoke 141 ``` 142 143 To setup invoke, you create a tasks.py file in the top directory of your project/package: 144 145 146 Here is an example *tasks.py* file: 147 148 ```python 149 """Invoke build and compile tasks. 150 151 """ 152 import os 153 from urllib.request import pathname2url 154 from invoke import Collection, task 155 import webbrowser 156 157 158 def open_browser(file_name: str): 159 """Open the default browser at a local html file.""" 160 uri = pathname2url(os.path.abspath(file_name)) 161 webbrowser.open(f"file://{uri}") 162 163 @task 164 def pip_install_requirements(c): 165 """Install dependencies needed to run packagename. 166 167 Runs 'pip install -r requirements.txt'. 168 """ 169 c.run('pip install -r requirements.txt') 170 171 172 @task(pip_install_requirements) 173 def pip_install_development_requirements(c): 174 """Install development dependencies needed to develop packagename. 175 176 Runs 'pip install -r requirements_dev.txt'. 177 """ 178 c.run('pip install -r requirements_dev.txt') 179 180 181 @task(pip_install_development_requirements) 182 def pip_install_emacs_requirements(c): 183 """Install all dependencies needed to develop with emacs as an IDE. 184 185 Runs 'pip install -r requirements_emacs_dev.txt'. 186 """ 187 c.run('pip install -r requirements_emacs_dev.txt') 188 189 190 @task(pip_install_development_requirements, default=True) 191 def pip_install_dev_mode(c): 192 """Install dev environment: required dev packages and also install locally in development mode. 193 194 Runs 'pip install -e .'. 195 """ 196 c.run('pip install -e .') 197 198 199 @task 200 def clean_release(c): 201 """Clean build artifacts produced when creating and releasing the package. 202 203 Runs remove file and directory commands to remove sdist and python egg files and directories. 204 """ 205 c.run('rm -rf build/') 206 c.run('rm -rf dist/') 207 c.run('rm -rf .eggs/') 208 c.run('rm -rf packagename.egg-info/') 209 c.run("find . -name '*.egg-info' -exec rm -fr {} +") 210 c.run("find . -name '*.egg' -exec rm -f {} +") 211 212 213 @task 214 def clean_pyc(c): 215 """Find and remove stale compiled python files ending in pyc or pyo as well as emacs backup files and __pycache__ directories.""" 216 c.run("find . -name '*.pyc' -exec rm -f {} +") 217 c.run("find . -name '*.pyo' -exec rm -f {} +") 218 c.run("find . -name '*~' -exec rm -f {} +") 219 c.run("find . -name '__pycache__' -exec rm -fr {} +") 220 221 222 @task 223 def clean_testing(c): 224 """Remove cruft created during testing activities, including stale coverage and pytest output.""" 225 c.run("rm -rf .tox/") 226 c.run("rm -f .coverage") 227 c.run("rm -rf htmlcov/") 228 c.run("rm -rf .pytest_cache") 229 230 231 @task 232 def clean_install(c): 233 """Uninstall packagename, regardless of original installation mode or method. 234 235 Runs 'pip uninstall -y packagename'. 236 """ 237 c.run("pip uninstall -y packagename") 238 239 240 @task(clean_release, clean_pyc, clean_testing, clean_install) 241 def pristine(c): 242 """Uninstall packagename and cleanup everything.""" 243 pass 244 245 246 @task(default=True) 247 def unit_test(c): 248 """Run unit tests. 249 250 Runs 'python -m unittest'. 251 """ 252 c.run("python -m unittest") 253 254 255 @task 256 def coverage(c): 257 """Run unit tests and collect code coverage statistics. 258 259 Runs: 260 'coverage run --source packagename -m unittest discover' 261 'coverage report -m' 262 'coverage html' 263 Then attempts to start a local browser pointed at ./htmlcov/index.html. 264 """ 265 c.run("coverage run --source packagename -m unittest discover") 266 c.run("coverage report -m") 267 c.run("coverage html") 268 open_browser("./htmlcov/index.html") 269 270 271 @task 272 def lint_bin(c): 273 """Run pylint code quality static analyzer on the contents of the ./bin directory.""" 274 c.run("pylint --fail-under=8 ./bin") 275 276 277 @task(lint_bin) 278 def lint(c): 279 """Run pylint code quality static analyzer on the contents of the ./packagename and ./bin directories.""" 280 c.run("pylint --fail-under=8 ./packagename") 281 282 283 @task(lint) 284 def lint_tests(c): 285 """Run pylint code quality static analyzer on the contents of the ./packagename, ./bin and ./test directories.""" 286 c.run("pylint --fail-under=2 ./tests") 287 288 289 @task 290 def security(c): 291 """Run bandit security static analyzer on the contents of the ./packagename directory.""" 292 c.run("bandit -r packagename") 293 294 295 @task 296 def docs(c): 297 """Generate API documentation in markdown format and overlay ./docs directory. 298 299 Done via pdoc3. 300 """ 301 c.run("pdoc3 --pdf packagename > docs/README.md") 302 303 304 @task(docs, default=True) 305 def release(c): 306 """Build a package suitable for redistribution. 307 308 Package can be uploaded to a PyPi compatible server such as an Azure DevOps Artifacts repository. 309 310 Runs 'python setup.py sdist' 311 """ 312 c.run("python setup.py sdist") 313 314 315 @task(docs) 316 def install_local(c): 317 """Install packagename locally via pip, see build.release and develop.install before running. 318 319 This does not install in dev mode and is used mainly for testing. Use build.release to promote and develop.install to develop. 320 321 Runs 'pip install .'. 322 """ 323 c.run("pip install ./") 324 325 ns = Collection() 326 327 quality = Collection('quality') 328 quality.add_task(coverage) 329 quality.add_task(lint) 330 quality.add_task(lint_tests) 331 quality.add_task(security) 332 quality.add_task(unit_test) 333 334 build = Collection('build') 335 build.add_task(clean_install, 'uninstall') 336 build.add_task(pristine) 337 build.add_task(install_local) 338 build.add_task(release) 339 340 develop = Collection('develop') 341 develop.add_task(pip_install_emacs_requirements, 'install-emacs') 342 develop.add_task(pip_install_dev_mode, 'install') 343 344 ns.add_collection(quality) 345 ns.add_collection(build) 346 ns.add_collection(develop) 347 348 ``` 349 350 More info on *invoke*: <https://www.pyinvoke.org>