<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Iacopo Spalletti</title>
    <description>The latest articles on Forem by Iacopo Spalletti (@yakky).</description>
    <link>https://forem.com/yakky</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F446904%2Fb5673810-6e87-4000-9acc-7b3dc7f25c6b.png</url>
      <title>Forem: Iacopo Spalletti</title>
      <link>https://forem.com/yakky</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/yakky"/>
    <language>en</language>
    <item>
      <title>Cleaning up you test suite with pytest parametrize</title>
      <dc:creator>Iacopo Spalletti</dc:creator>
      <pubDate>Sat, 26 Dec 2020 07:48:49 +0000</pubDate>
      <link>https://forem.com/yakky/cleaning-up-you-test-suite-with-pytest-parametrize-50cl</link>
      <guid>https://forem.com/yakky/cleaning-up-you-test-suite-with-pytest-parametrize-50cl</guid>
      <description>&lt;p&gt;I admit I jumped on the pytest ship quite late, after a few years of relying on the good ol’ unittest module, and for a good part they worked reasonably well for me, for little apparent incentive to something else.&lt;/p&gt;

&lt;p&gt;Over time I started using pytest more and more as I discovered quite a few interesting approaches available with it, which are out of unittest reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a test
&lt;/h2&gt;

&lt;p&gt;A single test function can be divided in three steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Arrange&lt;/strong&gt;: create the tests conditions and initialize the environment in which you want run the subject of the test&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Act&lt;/strong&gt;: execute the function you want to test&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Assert&lt;/strong&gt;: verify the function outcome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardest part of writing a test is by far the Arrange one.&lt;/p&gt;

&lt;p&gt;If you can get away with a limited setup in purely unit tests, as you move one step up in the testing pyramid, and you write integration or service tests, creating the correct conditions can be non trivial.&lt;/p&gt;

&lt;p&gt;The risk with this is to put too many assertions in a single test function to “spare” too many tests sharing the same (or similar) test arrangements.&lt;/p&gt;

&lt;p&gt;A slightly better solution is to share the arrangement between a few tests by calling a common function, but this can lead to make the tests less readable in the future due to the extra level of indirection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parametrize
&lt;/h2&gt;

&lt;p&gt;There is one thing computers are good at: executing repeatedly the same code against different inputs.&lt;/p&gt;

&lt;p&gt;What if we apply this to executing tests?&lt;/p&gt;

&lt;p&gt;That’s the idea of &lt;a href="https://docs.pytest.org/en/stable/parametrize.html"&gt;parametrize&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test_input,expected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s"&gt;"3+5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2+4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"6*9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks to how python works, you can treat a function as an input object to another function and let the latter alter the former or execute it with additional parameters (a pattern implemented by &lt;a href="https://docs.python.org/3/glossary.html#term-decorator"&gt;python decorators&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Parametrize uses this pattern to call our single test function using the list of inputs provided as arguments to effectively create (and the tests log reflects this) a list of test functions “generated” at runtime sharing the same body.&lt;/p&gt;

&lt;p&gt;The bonus of this approach is that you can concisely define a set of input and expectations, yet they are very clearly isolated from the test function body; on the contrary calling shared functions in multiple concrete test functions bury the conditions and expectations in the code making it difficult to quickly isolate them.&lt;/p&gt;

&lt;p&gt;A more complex example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"headers,status_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"bla bla"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"Token &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;LICENSE_API_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"no_header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"wrong_auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"authenticated"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_clear_cache_auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Cache is cleared if view is called with proper authentication token, is left intact otherwise. """&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"http://testserver"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"somekey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"someval"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_path_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cache_clear"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;
        &lt;span class="n"&gt;check_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"somekey"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;check_value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;check_value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;There are two things which one must be aware when using parametrize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  each test function run is executed in isolation, as if hey were different functions (as you would expect), so the arrange part is also execute multiple times. If it’s time consuming this will make your test suite slower very quickly as you add input combinations&lt;/li&gt;
&lt;li&gt;  you might to adapt the assertions depending on the expected outcome (if you are testing an email send function you may want to check the email headers of the delivered email the send is successful, which you can’t when testing for send failures): this can get out of hand very quickly and creating a mess of conditions and assertions, I advise against doing more than a single if to distinguish the normal tested function flow and the error handing; take this as signal that you are forcing too many unrelated assertions in a single test and evaluate whether create different test functions or test some of the assertions in other test functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't do this!&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;testdata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a,b,expected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testdata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timedistance_v0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" This function is intentionally bad. """&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interesting parametrize features
&lt;/h2&gt;

&lt;p&gt;Parametrize allows for very complex scenarios and I encourage you to check the &lt;a href="https://docs.pytest.org/en/stable/example/parametrize.html#paramexamples"&gt;documentation&lt;/a&gt; for the details.&lt;/p&gt;

&lt;p&gt;Still there are a couple of features worth mentioning&lt;/p&gt;

&lt;h3&gt;
  
  
  Assign ids to each combination
&lt;/h3&gt;

&lt;p&gt;By default parametrize creates the test function run name by combining the function name and the parameters set (to something like &lt;code&gt;test_timedistance_v0[a0-b0-expected0]&lt;/code&gt;) which might be self explanatory. To customize this you can use the ids argument to provide a terser name &lt;code&gt;test_timedistance_v0[forward]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Tests with automatic and manually specified ids:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;testdata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a,b,expected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testdata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timedistance_v0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Tests with autogenerated ids. """&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;


&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a,b,expected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testdata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"forward"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"backward"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timedistance_v1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;""" Tests with customized ids. """&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated test names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pytest test_time.py &lt;span class="nt"&gt;--collect-only&lt;/span&gt;
&lt;span class="o"&gt;===========================&lt;/span&gt; &lt;span class="nb"&gt;test &lt;/span&gt;session starts &lt;span class="o"&gt;============================&lt;/span&gt;
platform linux &lt;span class="nt"&gt;--&lt;/span&gt; Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: &lt;span class="nv"&gt;$PYTHON_PREFIX&lt;/span&gt;/.pytest_cache
rootdir: &lt;span class="nv"&gt;$REGENDOC_TMPDIR&lt;/span&gt;
collected 8 items

&amp;lt;Module test_time.py&amp;gt;
  &amp;lt;Function test_timedistance_v0[a0-b0-expected0]&amp;gt;
  &amp;lt;Function test_timedistance_v0[a1-b1-expected1]&amp;gt;
  &amp;lt;Function test_timedistance_v1[forward]&amp;gt;
  &amp;lt;Function test_timedistance_v1[backward]&amp;gt;

&lt;span class="o"&gt;========================&lt;/span&gt; 4 tests collected &lt;span class="k"&gt;in &lt;/span&gt;0.12s &lt;span class="o"&gt;========================&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mark parameters combination
&lt;/h3&gt;

&lt;p&gt;One of the great pytest features are marker which allow to decorate test functions for different behavior (parametrize itself is a marker). You can use marks on a parametrize decorated function as you would normally do, but you can also decorate single parameter set, using the pytest.param function, which you can also use to define a single Id without defined one for each combination&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"test_input,expected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"3+5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2+4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"basic_2+4"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"6*9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xfail&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"basic_6*9"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Take home
&lt;/h2&gt;

&lt;p&gt;One of the signatures of pytest is to completely embrace the dynamic nature of python, which allows it to work on a “meta” level, by enriching the test behaviors working outside the test functions, thus separating very clearly the boundaries and creating a test suite easier to understand and extended.&lt;/p&gt;

&lt;p&gt;Parametrize is a perfect example of the pytest approach and I advise you to experiment with it, as it will greatly improve your test writing experience.&lt;/p&gt;

</description>
      <category>pytest</category>
      <category>testing</category>
      <category>python</category>
    </item>
    <item>
      <title>My 2020 Python linting setup: How I Learned to Stop Worrying and Love the automated code formatting</title>
      <dc:creator>Iacopo Spalletti</dc:creator>
      <pubDate>Fri, 18 Dec 2020 13:34:40 +0000</pubDate>
      <link>https://forem.com/yakky/my-2020-python-linting-setup-how-i-learned-to-stop-worrying-and-love-the-automated-code-formatting-1d1d</link>
      <guid>https://forem.com/yakky/my-2020-python-linting-setup-how-i-learned-to-stop-worrying-and-love-the-automated-code-formatting-1d1d</guid>
      <description>&lt;h2&gt;
  
  
  Prologue
&lt;/h2&gt;

&lt;p&gt;No matter if a team works using pair programming or by requiring code review before checking in on the main branch, code formatting is a constant source of distraction, when not a source of discussion.&lt;/p&gt;

&lt;p&gt;Writing even a few lines of code requires a lot of code style decisions, which, if not agreed in advance, yields a lot of bickering and hard to read code review diff.&lt;/p&gt;

&lt;p&gt;The obvious step is to establish team-wide style guide, to avoid arguments and limit messy diff.&lt;/p&gt;

&lt;p&gt;But we are developers, we are lazy by nature, and there is little immediate reward in constantly reformatting by hand the code you are writing. You know it matters, but it’s a burden (and an unwise way to spend time) to do it manually.&lt;/p&gt;

&lt;p&gt;I have been using a style guide for a very long time, and very early on I started pushing checks in the CI.&lt;/p&gt;

&lt;p&gt;But I still felt I was lacking a ton of automation, so I started studying the tools that would near me to the complete automation goal.&lt;/p&gt;

&lt;p&gt;I want to share my findings in this quest, that might be useful for others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting expectations
&lt;/h2&gt;

&lt;p&gt;Automation comes at a cost: you depend on the style rules available in existing linting/formatting tools (unless you want to write one), and you have to balance what you think as “good” code, with what the tools can achieve.&lt;/p&gt;

&lt;p&gt;So the first, less pleasing, part of the process is going through the existing tools, and choosing the ones closer to what you want, and then tweaking their output to suit your style; but in the end you will have to accept a few compromises. It took me a while to get along with this (and it’s one of the reasons why it’s been a long process to me).&lt;/p&gt;

&lt;p&gt;After a few months of tests with real projects, I think I found a pretty good balance, and I learnt to live with the less pleasing outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the checks
&lt;/h2&gt;

&lt;p&gt;As we depend on tools to handle all the checks, we must decide when to run them.&lt;/p&gt;

&lt;p&gt;As everyone in the team is free to use their editor, we decided to keep the checking / formatting configuration in the repository itself, as an easy way to use exactly the same tools without relying on mixed support from each editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI
&lt;/h3&gt;

&lt;p&gt;This made also very trivial to run the same checks in the continuous integration pipeline, as we want to run the checks on every push to ensure code is sane when you open the pull request and avoid he annoying code style comments during the review. Over the years and I have used &lt;a href="https://sleepy.yaks.industries/posts/python-linting-setup-2020/gitlab-ci.yml"&gt;gitlab-ci&lt;/a&gt;, travis, &lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/.github/workflows/lint.yml"&gt;github actions&lt;/a&gt; and others. As I wrapped all the checks in [tox environments]((&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/tox.ini"&gt;https://github.com/nephila/djangocms-blog/blob/develop/tox.ini&lt;/a&gt;) the CI specific details didn’t matter and I ported configurations across CI environments with zero effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://pre-commit.com/"&gt;Pre-commit&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/.pre-commit-config.yaml"&gt;Example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But before pushing, you want to make sure everything passes,to avoid wasting CI resources, and your time. For a long time, I relied upon simply running tox locally, which was effective but tedious. And then I decided to give a read to the pre-commit documentation and it made my life so much easier.&lt;br&gt;
Pre-commit create a pre-commit git hook and launch a set of checks on every file staged for commit, before it’s committed: this ensure that committed files respect the configured checks.&lt;br&gt;
This is really game changing, because you can use your coding workflow without any extra step required for formatting and linting: edit, save, add and commit, and all the changes will be validated.&lt;br&gt;
Apart from adding the standalone tools, pre-commit comes with a lot of integrated checks (which partially overlaps with the others, but the added value is totally worth the extra steps) (see &lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/.pre-commit-config.yaml#L7"&gt;config file&lt;/a&gt; for the configured checks and &lt;a href="https://github.com/pre-commit/pre-commit-hooks#hooks-available"&gt;pre-commit hooks documentation&lt;/a&gt; for the detail).&lt;br&gt;
For projects where python and JavaScript code are in the same repository, JavaScript &lt;code&gt;lint-staged&lt;/code&gt; command is also integrated as a pre-commit tool, to run the frontend linting together with the python one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Code style and format
&lt;/h3&gt;

&lt;p&gt;This is of course the core of a linting suite, and where the team consensus can be somewhat harder to reach.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://editorconfig.org/"&gt;Editorconfig&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/.editorconfig"&gt;Example&lt;/a&gt;&lt;br&gt;
Editorconfig is a specification for basic text formatting (line length, indentation rules etc) which is used by the text editors and IDEs as a way to configure editor automation. It’s not python specific and it only provides basic rules, but it’s very handy as it’s basically transparent, once you enable editorconfig support in your editor.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://flake8.pycqa.org/en/latest/"&gt;flake8&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/tox.ini#L126"&gt;Example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Flake8 is the first tool I introduced quite a while ago, and it does quite a good job.&lt;/p&gt;

&lt;p&gt;It has a very comprehensive set of rules to check the code for style or logic errors. It covers a lot of non-trivial cases, and it brings a much more consistent code.&lt;/p&gt;

&lt;p&gt;In this iteration I added a lot of plugins for quite a strict configuration, even if most of the work is done by black, this still find quite a few quirks worth of being fixed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-broken-line/"&gt;flake8-broken-line&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Report usage of &lt;code&gt;\&lt;/code&gt; to break new lines, use &lt;code&gt;()&lt;/code&gt; instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-bugbear/"&gt;flake8-bugbear&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;very opinionated -but very sensible- syntactic and logic checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-builtins/"&gt;flake8-builtins&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Report local variables shadowing python builtins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-coding/"&gt;flake8-coding&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Used to reject encoding magic comments in files: as codebase is now py3 only, we don’t need those&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-commas/"&gt;flake8-commas&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Check trailing commas, it has some compatibility issue with black&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-comprehensions/"&gt;flake8-comprehensions&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Advise about usage of comprehensions and generators to help writing more idiomatic code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-eradicate/"&gt;flake8-eradicate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Report commented out code: we don’t need it and we can retrieve older versions of the code from git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-quotes/"&gt;flake8-quotes&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Check quote consistency, somewhat obsolete due to black formatting quotes according to its rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/flake8-tidy-imports/"&gt;flake8-tidy-imports&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Enforce some rules for module imports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://pypi.org/project/isort/"&gt;isort&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/tox.ini#L137"&gt;Example&lt;/a&gt;&lt;br&gt;
Stop worrying about organising imports has been one of the easiest victory I achieved in my quest for the perfect linting setup.&lt;/p&gt;

&lt;p&gt;isort is basically &lt;strong&gt;the&lt;/strong&gt; tool you want to use to organise python imports, it does a great and consistent job, with many different styles to suit any preference, and once you set it up it just runs and do its job. Only occasionally I found weird inconsistencies, but they can be easily fixed. Beware if using with black, as the import formatting style must match the one expected by black.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://pypi.org/project/black/"&gt;black&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Black is a very much touted tool nowadays, and for good reasons.&lt;/p&gt;

&lt;p&gt;It’s fast, it’s easy to use, and it’s really consistent.&lt;/p&gt;

&lt;p&gt;It does a great job at formatting the code according to the code style, and I have yet to find an error (as in code that doesn’t run or with black introduced bug).&lt;/p&gt;

&lt;p&gt;On the other hand it’s totally opinionated to the point that you barely can configure it. It took me a while to digest this, even more as some of its style decisions look questionable to me. Yet, the output is good enough and the amount of work it lift is so much, that I think it would be stupid not to use it.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://github.com/asottile/pyupgrade"&gt;pyupgrade&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;My latest discovery is this tool (which is currently only integrated in &lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/.pre-commit-config.yaml#L49"&gt;pre-commit&lt;/a&gt;), which detects and improve the syntax according to the features available in the target python version. Going through its documentation is also very instructive to learn more idiomatic python.&lt;/p&gt;

&lt;h3&gt;
  
  
  Documentation
&lt;/h3&gt;

&lt;p&gt;One of the most overlooked part of developing the software is the inline documentation (with narrative documentation being &lt;strong&gt;the&lt;/strong&gt; most overlooked part), so a gentle (or not so gentle) nudge toward writing documentation is really important.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://www.sphinx-doc.org"&gt;Sphinx&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;The most basic check for your documentation is that it compiles successfully, by adding Sphinx building to the pipeline checks, we ensure the correct syntax. On github you can just configure &lt;a href="https://docs.readthedocs.io/en/latest/guides/autobuild-docs-for-pull-requests.html"&gt;building documentation on each pull request&lt;/a&gt; via readthedocs, which is definitely the best way to test the documentation as it provides a browsable fully rendered version for proofreading and other non automated checks.&lt;/p&gt;

&lt;p&gt;On GitLab you can add the relevant job &lt;a href="https://sleepy.yaks.industries/posts/python-linting-setup-2020/gitlab-ci.yml"&gt;line 13&lt;/a&gt; ensuring the &lt;code&gt;-W&lt;/code&gt; flag is used to treat warnings as errors.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://pypi.org/project/pydocstyle/"&gt;pydocstyle&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://sleepy.yaks.industries/posts/python-linting-setup-2020/tox.ini"&gt;Example&lt;/a&gt;&lt;br&gt;
Pydocstyle checks the presence of doc strings at different levels (module, classes, methods, etc), the formatting and its wording style.&lt;/p&gt;

&lt;p&gt;Its default configuration is quite aggressive, and it required me quite a few try to balance automatic check stubborness with actual useful documentation.&lt;/p&gt;

&lt;p&gt;It’s still somewhat the weakest link in this setup. Maybe because I don’t write enough documentation?🤔&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://pypi.org/project/interrogate/"&gt;interrogate&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/pyproject.toml#L16"&gt;Example&lt;/a&gt;&lt;br&gt;
It’s a very recent addition and I am still evaluating it (by setting its failure threshold to 0).&lt;/p&gt;

&lt;p&gt;On one hand its logic is simpler than pydocstyle: it merely count the number of docstrings-covered “objects” (module, classes, methods, etc).&lt;/p&gt;

&lt;p&gt;But this approach provides a more nuanced approach at covering code with documentation, as it allows to avoid adding stub / void doc strings where not needed, or when working on an existing codebase.&lt;/p&gt;

&lt;p&gt;My only current concern is its unnecessary failure when an excluded directory does not exist, which leads to more maintenance. I plan to propose a PR to fix this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package information
&lt;/h3&gt;

&lt;p&gt;If only I can count the botched pypi uploads due to errors in manifest file or other package meta information (namely correct rst formatting in package long description). While not fundamental, they are a good safeguards for easier applications maintainer life.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://pypi.org/project/check-manifest/"&gt;check-manifest&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/tox.ini#L148"&gt;Example&lt;/a&gt;&lt;br&gt;
Does exactly what its name says, it checks the manifest file against the repo files and report any file not included in manifest declarations.&lt;br&gt;
Even in mature applications, it saved me in a couple of occasions; it has a bit of setup/maintenance work at the beginning as you will have to accurately check which files legitimately are not going in the package file.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://pypi.org/project/pep517/"&gt;pep517&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/tox.ini#L106"&gt;Example&lt;/a&gt;&lt;br&gt;
If you are going to release your application as a package, you’d better check that it actually builds. Among other things, pep517 does exactly that.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://pypi.org/project/twine/"&gt;twine&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/nephila/djangocms-blog/blob/develop/tox.ini#L107"&gt;Example&lt;/a&gt;&lt;br&gt;
I use twine to upload built packages to pypi or private registries, but it also provides a command to check the long description syntax, which is really important as pypi is now refusing packages with broken long description, and you don’t want to do multiple releases because of a syntax typo somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue
&lt;/h2&gt;

&lt;p&gt;Moving to auto formatted and strictly checked codebase helped me a lot focusing more on the substance of writing code (design and architecture) and less on the formal details.&lt;/p&gt;

&lt;p&gt;When working with a team, it also significantly reduced the time for a code review and to get a pull request merged, as one can focus on the implementation by having a cleaner diff and a consistent code.&lt;/p&gt;

&lt;p&gt;The journey is far from over, as there is room for improvements in the current setup (starting from checking overlapping between flake8 and black), but overall it is proving a solid base with interesting returns.&lt;/p&gt;

</description>
      <category>python</category>
      <category>linting</category>
      <category>formatting</category>
      <category>automation</category>
    </item>
    <item>
      <title>TIL - 🧹 unimport linter formatter</title>
      <dc:creator>Iacopo Spalletti</dc:creator>
      <pubDate>Sun, 13 Dec 2020 09:44:46 +0000</pubDate>
      <link>https://forem.com/yakky/til-unimport-linter-formatter-2efp</link>
      <guid>https://forem.com/yakky/til-unimport-linter-formatter-2efp</guid>
      <description>&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q72bCdRT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/907277477935964161/ZSxasCvw_normal.jpg" alt="Jeff Triplett 😷 profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Jeff Triplett 😷
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @webology
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P4t6ys1m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      ✨ Unimport - A linter &amp;amp; formatter for finding &amp;amp; removing unused import statements. - looks good to me. If it works in large codebases as well as it just did on a few smaller projects for me then I'll add it to my toolbelt. &lt;br&gt;&lt;br&gt;&lt;a href="https://t.co/yk1X2UMemX"&gt;unimport.hakancelik.dev/#/&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      23:38 PM - 12 Dec 2020
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1337904660137512963" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-reply-action.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1337904660137512963" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-retweet-action.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      0
      &lt;a href="https://twitter.com/intent/like?tweet_id=1337904660137512963" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-like-action.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
      10
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;p&gt;Thanks to this Jeff Triplets tweet I discovered &lt;a href="https://unimport.hakancelik.dev/#/"&gt;unimport&lt;/a&gt;, which is an interesting tool to detect unused imports (like flake8 would do) and to &lt;strong&gt;remove&lt;/strong&gt; them from the code.&lt;/p&gt;

&lt;p&gt;And the cherry on top it's that it can scan the requirements file and detect unused dependencies (which is probably even more important).&lt;/p&gt;

&lt;p&gt;Historically this has not been a problem for me (or Nephila team), as flake8 is already well ingrained in our workflow, so we are used to handle &lt;a href="https://sleepy.yaks.industries/posts/python-linting-setup-2020/"&gt;code cleanup&lt;/a&gt; before committing.&lt;/p&gt;

&lt;p&gt;But as I sometimes inherit a large codebase with messy code, and it's awesome how much you can achieve with the right tools.&lt;/p&gt;

&lt;p&gt;Currently &lt;code&gt;isort&lt;/code&gt; + &lt;code&gt;black&lt;/code&gt; + &lt;code&gt;pyupgrade&lt;/code&gt; already do a really good job, but adding &lt;code&gt;unimport&lt;/code&gt;  to the equation is 💥: a much nicer codebase with zero effort&lt;/p&gt;

&lt;p&gt;I will surely give it a more in depth test, and I might add to my &lt;a href="https://sleepy.yaks.industries/posts/python-linting-setup-2020/"&gt;linting reference toolbox&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Shootout to &lt;a href="//hakancelik.dev"&gt;Hakan Çelik&lt;/a&gt; for this package, and its awesome documentation.&lt;/p&gt;

</description>
      <category>python</category>
      <category>linting</category>
      <category>formatting</category>
    </item>
    <item>
      <title>Converting a django CMS plugin to a different model</title>
      <dc:creator>Iacopo Spalletti</dc:creator>
      <pubDate>Thu, 13 Aug 2020 08:01:26 +0000</pubDate>
      <link>https://forem.com/yakky/converting-a-django-cms-plugin-to-a-different-model-2h13</link>
      <guid>https://forem.com/yakky/converting-a-django-cms-plugin-to-a-different-model-2h13</guid>
      <description>&lt;p&gt;Once in a while you might find yourself in the situation in which a django CMS plugin you are using in a project is no longer supported or no longer suited for your goals.&lt;br&gt;
But then, you have hundreds (or thousands) of instances of the old plugin and you need a quick way to migrate all the data to a new one.&lt;br&gt;
Luckily, it's possible to achieve this using a rather simple data migration.&lt;/p&gt;

&lt;p&gt;Sample code used referenced below: &lt;a href="https://gist.github.com/yakky/31466588da712e25e1f2c200d56d3776"&gt;https://gist.github.com/yakky/31466588da712e25e1f2c200d56d3776&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;In order to have a successful migration we must achieve the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the plugins tree consistent&lt;/li&gt;
&lt;li&gt;keep the reference to the plugin valid (eg: the tag embedded in text fields when the plugin is embeddable in a text field)&lt;/li&gt;
&lt;li&gt;keep custom data intact (at least the ones supported by the new plugin model)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To achieve this we can use the fact that Django models instances are python classes disconnected from the database until an explicit database update is run.&lt;/p&gt;

&lt;h3&gt;
  
  
  deceiving Django ORM and the database
&lt;/h3&gt;

&lt;p&gt;So the first step (lines 9-10) is to create an instance of the new plugin class &lt;em&gt;without&lt;/em&gt; saving it to the database, then set the id of the new plugin to the one that's being removed; at the end (21-22) we delete the old instance before saving the new one to free any constraint on unique fields.&lt;br&gt;
By keeping the same id, plugin instances embedded in text plugins (whose reference is kept by hardcoding the id in the text body) are moved to the new model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating tree data
&lt;/h3&gt;

&lt;p&gt;Django CMS plugins are stored in the database using the materialized path algorithm by Django-treebeard, and the plugins hierarchy information is stored in a few fields which neees to be copied verbatim (lines 11-18). This bypass the tree algorithm and we effectively replace a single node in the tree without affecting the tree balancement or triggering the automatic tree rebalance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating custom data
&lt;/h3&gt;

&lt;p&gt;We now can save the custom model data to the new one. To achieve this we can add the lists of assignments from the field on the old plugin to the one the new plugin: &lt;code&gt;new_plugin.field_name = old_plugin.field_name&lt;/code&gt;. If the plugin has any many to many relationship we can copy those as well, after saving our new plugin, using &lt;code&gt;new_plugin.many2many.set(old_plugin.many2many.all())&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The above snippet has still room for improvements (like using &lt;code&gt;_meta.get_fields&lt;/code&gt; to copy the fields data without writing every single field, but should provide you a good starting point to customise them your needs.&lt;/p&gt;

</description>
      <category>django</category>
      <category>djangocms</category>
    </item>
  </channel>
</rss>
