PageRenderTime 77ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 1ms

/atom.xml

https://github.com/treyhunner/treyhunner.github.com
XML | 1170 lines | 866 code | 304 blank | 0 comment | 0 complexity | f5fc078b550c625035b3083532e1990f MD5 | raw file
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <feed xmlns="http://www.w3.org/2005/Atom">
  3. <title><![CDATA[Trey Hunner]]></title>
  4. <link href="http://treyhunner.com/atom.xml" rel="self"/>
  5. <link href="http://treyhunner.com/"/>
  6. <updated>2014-06-16T18:24:10-07:00</updated>
  7. <id>http://treyhunner.com/</id>
  8. <author>
  9. <name><![CDATA[Trey Hunner]]></name>
  10. </author>
  11. <generator uri="http://octopress.org/">Octopress</generator>
  12. <entry>
  13. <title type="html"><![CDATA[CLI for Finding DRM-free Audiobooks]]></title>
  14. <link href="http://treyhunner.com/2014/05/cli-for-finding-drm-free-audiobooks/"/>
  15. <updated>2014-05-14T12:31:00-07:00</updated>
  16. <id>http://treyhunner.com/2014/05/cli-for-finding-drm-free-audiobooks</id>
  17. <content type="html"><![CDATA[<p>I recently acquired an appreciation for audiobooks. I listen to multiple
  18. audiobooks every month and I prefer DRM-free audiobooks so that I can listen
  19. through my favorite audiobook reader on each of my devices.</p>
  20. <p><a href="http://www.downpour.com/">Downpour</a> and <a href="http://www.emusic.com/">eMusic</a> are the only services I know that provide a large
  21. selection of DRM-free audiobooks. Unfortunately not all audiobooks are
  22. available on either of these sites, so I often end up searching both sites for
  23. each book to discover what versions (if any) are available from each. I got
  24. sick of searching both websites all the time, so I just created a Python script
  25. to do that work for me.</p>
  26. <div><script src='https://gist.github.com/21f307b975027be5162d.js'></script>
  27. <noscript><pre><code>#!/usr/bin/env python
  28. &quot;&quot;&quot;
  29. Search downpour.com and emusic.com for DRM-free audiobooks
  30. Usage::
  31. ./find_audiobooks.py &lt;title&gt;...
  32. File released to the public domain under CC0 license:
  33. http://creativecommons.org/publicdomain/zero/1.0/deed
  34. &quot;&quot;&quot;
  35. import sys
  36. from itertools import chain, izip_longest
  37. import urllib2
  38. from bs4 import BeautifulSoup
  39. from purl import URL
  40. def unescape(text):
  41. &quot;&quot;&quot;Return string without smart apostrophes&quot;&quot;&quot;
  42. return text.replace(u'\u2019', &quot;'&quot;)
  43. def get_downpour_url(book_name):
  44. &quot;&quot;&quot;Return search URL for downpour.com&quot;&quot;&quot;
  45. base = URL(&quot;http://www.downpour.com/catalogsearch/result/&quot;)
  46. return base.query_param('q', book_name).as_string()
  47. def get_emusic_url(book_name):
  48. &quot;&quot;&quot;Return search URL for emusic.com&quot;&quot;&quot;
  49. base_url = URL(&quot;http://www.emusic.com/search/book/&quot;)
  50. return base_url.query_param('s', book_name).as_string()
  51. def search_downpour(book_name):
  52. &quot;&quot;&quot;Search Downpour and return list of parsed results&quot;&quot;&quot;
  53. response = urllib2.urlopen(get_downpour_url(book_name))
  54. page = BeautifulSoup(response)
  55. books = page.find_all('li', attrs={'class': &quot;item&quot;})
  56. results = []
  57. for book in books:
  58. header = book.find(attrs={'class': &quot;product-name&quot;})
  59. link_tag = header.find('a')
  60. title = ' '.join(unescape(x)
  61. for x in link_tag.stripped_strings)
  62. link = link_tag['href']
  63. for author in book.find_all(attrs={'class': 'author'}):
  64. author_text = author.text
  65. if author_text.startswith('By '):
  66. author = author_text[3:]
  67. results.append({
  68. 'title': title,
  69. 'link': link,
  70. 'author': author,
  71. })
  72. return results
  73. def search_emusic(book_name):
  74. &quot;&quot;&quot;Search eMusic and return list of parsed results&quot;&quot;&quot;
  75. response = urllib2.urlopen(get_emusic_url(book_name))
  76. page = BeautifulSoup(response)
  77. books = page.find_all('li', attrs={'class': &quot;bundle&quot;})
  78. results = []
  79. for book in books:
  80. link_tag = book.find('h4').find('a')
  81. author_tag = book.find('h5')
  82. results.append({
  83. 'title': link_tag.text,
  84. 'link': link_tag['href'],
  85. 'author': author_tag.text,
  86. })
  87. return results
  88. def print_result(result):
  89. &quot;&quot;&quot;Print title, author, and link for audiobook result&quot;&quot;&quot;
  90. print &quot;Title: {}&quot;.format(result['title'])
  91. print &quot;Author: {}&quot;.format(result['author'])
  92. print &quot;Link: {}&quot;.format(result['link'])
  93. print
  94. def merge_lists(*lists):
  95. &quot;&quot;&quot;Return merge of lists by alternating elements of each&quot;&quot;&quot;
  96. combined_lists = chain.from_iterable(izip_longest(*lists))
  97. return list(filter(bool, combined_lists))
  98. def main(*args):
  99. &quot;&quot;&quot;Search audiobook sites and return search results&quot;&quot;&quot;
  100. for book_name in args:
  101. results1 = search_emusic(book_name)
  102. results2 = search_downpour(book_name)
  103. results = merge_lists(results1[:3], results2[:3])
  104. for result in results:
  105. print_result(result)
  106. if __name__ == &quot;__main__&quot;:
  107. main(*sys.argv[1:])
  108. </code></pre></noscript></div>
  109. <h3>Future Improvements</h3>
  110. <p>Some ideas for future improvements:</p>
  111. <ul>
  112. <li>Add <a href="http://www.audible.com/">Audible</a> results when the book cannot be found in a DRM-free format</li>
  113. <li>Rewrite the script in JavaScript and create a Chrome extension out of it</li>
  114. <li>Use <a href="https://github.com/kennethreitz/clint">clint</a> to colorize the command-line output</li>
  115. </ul>
  116. ]]></content>
  117. </entry>
  118. <entry>
  119. <title type="html"><![CDATA[Supporting Both Django 1.7 and South]]></title>
  120. <link href="http://treyhunner.com/2014/03/migrating-to-django-1-dot-7/"/>
  121. <updated>2014-03-27T13:05:00-07:00</updated>
  122. <id>http://treyhunner.com/2014/03/migrating-to-django-1-dot-7</id>
  123. <content type="html"><![CDATA[<p>Have an open source Django app with South migrations? Adding support for Django 1.7 might be a little painful. In this post I will discuss the difficulty of supporting Django 1.7 while maintaining South migrations for users of Django 1.6 and below.</p>
  124. <p>Django 1.7 uses the <code>migrations</code> sub-package in your app for database migrations and South relies on the same package. Unfortunately, you can&rsquo;t store both packages in the same place. At first glance, it seems we cannot support both Django 1.7 and previous versions of Django using South. However, as I explain below, we can support both at once.</p>
  125. <h2>Assessing your options</h2>
  126. <p>In order to support both Django 1.7 and Django 1.6 with South we can rename the <code>migrations</code> package and instruct users to reference the new package in their settings module. We can do this with the <a href="https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-MIGRATION_MODULES">MIGRATION_MODULES</a> or <a href="http://south.readthedocs.org/en/latest/settings.html#south-migration-modules">SOUTH_MIGRATION_MODULES</a> settings. There are three options:</p>
  127. <ol>
  128. <li>Move existing <code>migrations</code> directory to <code>south_migrations</code> and create Django 1.7 migrations in <code>migrations</code> package</li>
  129. <li>Create new Django 1.7 migrations package in <code>django_migrations</code> directory and leave existing South migrations package</li>
  130. <li>Move existing <code>migrations</code> directory to <code>south_migrations</code> and create Django 1.7 migrations in <code>django_migrations</code> directory</li>
  131. </ol>
  132. <p>The first option requires existing users either switch to Django 1.7 or update their settings module before upgrading to the new version of your app. The second option requires all Django 1.7 users to customize their settings module to properly install your app. The third option requires everyone (both Django 1.7 and South users) to update their settings module.</p>
  133. <p>Out of those options I prefer the first one. When you eventually drop support for South, you will probably want your Django 1.7 migrations to live in the <code>migrations</code> directory. If you don&rsquo;t force that switch now, you would eventually need to break backwards-compatibility or maintain two duplicate migrations directories.</p>
  134. <p>So our plan is to move the South migrations to <code>south_migrations</code> and create Django 1.7 migrations. An example with the <a href="https://github.com/treyhunner/django-email-log">django-email-log</a> app:</p>
  135. <figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  136. <span class='line-number'>2</span>
  137. <span class='line-number'>3</span>
  138. </pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>git mv email_log/migrations email_log/south_migrations
  139. </span><span class='line'><span class="nv">$ </span>python manage.py makemigrations email_log
  140. </span><span class='line'><span class="nv">$ </span>git add email_log/migrations
  141. </span></code></pre></td></tr></table></div></figure>
  142. <h2>Breaking South support</h2>
  143. <p>If you move <code>migrations</code> to <code>south_migrations</code> and make a Django 1.7 <code>migrations</code> package, what happens to existing users with South?</p>
  144. <p>Your new app upgrade will break backwards compatibility for South users and you want to make sure they <em>know</em> they need to make a change immediately after upgrading. Users should see a loud and clear error message instructing them what they need to do. This can be done by hijacking their use of the <strong>migrate</strong> command with South.</p>
  145. <p>Existing users will run <strong>migrate</strong> when upgrading your app. If they don&rsquo;t migrate immediately, they will when they notice a problem and realize they need to run <strong>migrate</strong>. Upon migrating, we want to show a clear error message telling the user what to do.</p>
  146. <h2>Failing loudly and with a clear error message</h2>
  147. <p>When South looks for app migrations it will import our <code>migrations</code> package. Our <code>migrations</code> package contains Django 1.7 migrations, which South won&rsquo;t understand. So we want to make sure that if our <code>migrations</code> package is imported either Django 1.7 is installed or a proper error message is displayed. Upon importing this package, we can check for the presence of the new <code>django.db.migrations</code> module and if not found we will raise an exception with a descriptive error message.</p>
  148. <p>For example, this is the code I plan to add to the <code>email_log/migrations/__init__.py</code> file for <a href="https://github.com/treyhunner/django-email-log">django-email-log</a> to add Django 1.7 support:</p>
  149. <figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  150. <span class='line-number'>2</span>
  151. <span class='line-number'>3</span>
  152. <span class='line-number'>4</span>
  153. <span class='line-number'>5</span>
  154. <span class='line-number'>6</span>
  155. <span class='line-number'>7</span>
  156. <span class='line-number'>8</span>
  157. <span class='line-number'>9</span>
  158. <span class='line-number'>10</span>
  159. <span class='line-number'>11</span>
  160. <span class='line-number'>12</span>
  161. <span class='line-number'>13</span>
  162. <span class='line-number'>14</span>
  163. <span class='line-number'>15</span>
  164. <span class='line-number'>16</span>
  165. <span class='line-number'>17</span>
  166. <span class='line-number'>18</span>
  167. <span class='line-number'>19</span>
  168. <span class='line-number'>20</span>
  169. <span class='line-number'>21</span>
  170. </pre></td><td class='code'><pre><code class='python'><span class='line'><span class="sd">&quot;&quot;&quot;</span>
  171. </span><span class='line'><span class="sd">Django migrations for email_log app</span>
  172. </span><span class='line'>
  173. </span><span class='line'><span class="sd">This package does not contain South migrations. South migrations can be found</span>
  174. </span><span class='line'><span class="sd">in the ``south_migrations`` package.</span>
  175. </span><span class='line'><span class="sd">&quot;&quot;&quot;</span>
  176. </span><span class='line'>
  177. </span><span class='line'><span class="n">SOUTH_ERROR_MESSAGE</span> <span class="o">=</span> <span class="s">&quot;&quot;&quot;</span><span class="se">\n</span><span class="s"></span>
  178. </span><span class='line'><span class="s">For South support, customize the SOUTH_MIGRATION_MODULES setting like so:</span>
  179. </span><span class='line'>
  180. </span><span class='line'><span class="s"> SOUTH_MIGRATION_MODULES = {</span>
  181. </span><span class='line'><span class="s"> &#39;email_log&#39;: &#39;email_log.south_migrations&#39;,</span>
  182. </span><span class='line'><span class="s"> }</span>
  183. </span><span class='line'><span class="s">&quot;&quot;&quot;</span>
  184. </span><span class='line'>
  185. </span><span class='line'><span class="c"># Ensure the user is not using Django 1.6 or below with South</span>
  186. </span><span class='line'><span class="k">try</span><span class="p">:</span>
  187. </span><span class='line'> <span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">migrations</span> <span class="c"># noqa</span>
  188. </span><span class='line'><span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
  189. </span><span class='line'> <span class="kn">from</span> <span class="nn">django.core.exceptions</span> <span class="kn">import</span> <span class="n">ImproperlyConfigured</span>
  190. </span><span class='line'> <span class="k">raise</span> <span class="n">ImproperlyConfigured</span><span class="p">(</span><span class="n">SOUTH_ERROR_MESSAGE</span><span class="p">)</span>
  191. </span></code></pre></td></tr></table></div></figure>
  192. <p>Now when we run <strong>migrate</strong> with Django 1.6 and South, we&rsquo;ll see the following exception raised:</p>
  193. <figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  194. <span class='line-number'>2</span>
  195. <span class='line-number'>3</span>
  196. <span class='line-number'>4</span>
  197. <span class='line-number'>5</span>
  198. <span class='line-number'>6</span>
  199. <span class='line-number'>7</span>
  200. </pre></td><td class='code'><pre><code class='python'><span class='line'><span class="n">django</span><span class="o">.</span><span class="n">core</span><span class="o">.</span><span class="n">exceptions</span><span class="o">.</span><span class="n">ImproperlyConfigured</span><span class="p">:</span>
  201. </span><span class='line'>
  202. </span><span class='line'><span class="n">For</span> <span class="n">South</span> <span class="n">support</span><span class="p">,</span> <span class="n">customize</span> <span class="n">the</span> <span class="n">SOUTH_MIGRATION_MODULES</span> <span class="n">setting</span> <span class="n">like</span> <span class="n">so</span><span class="p">:</span>
  203. </span><span class='line'>
  204. </span><span class='line'> <span class="n">SOUTH_MIGRATION_MODULES</span> <span class="o">=</span> <span class="p">{</span>
  205. </span><span class='line'> <span class="s">&#39;email_log&#39;</span><span class="p">:</span> <span class="s">&#39;email_log.south_migrations&#39;</span><span class="p">,</span>
  206. </span><span class='line'> <span class="p">}</span>
  207. </span></code></pre></td></tr></table></div></figure>
  208. <h2>Conclusion</h2>
  209. <p>This breaks backwards compatibility, but our users should immediately understand what has broken and how to fix it. Remember to upgrade the major number of your package version to note this backwards-incompatible change.</p>
  210. <p>I would love to hear your thoughts about this approach in the comments below. Let me know if you have other ideas about how to handle supporting Django 1.7 migrations and South at the same time.</p>
  211. ]]></content>
  212. </entry>
  213. <entry>
  214. <title type="html"><![CDATA[TDD With Django Tutorial]]></title>
  215. <link href="http://treyhunner.com/2013/11/tdd-with-django-workshop/"/>
  216. <updated>2013-11-04T00:00:00-08:00</updated>
  217. <id>http://treyhunner.com/2013/11/tdd-with-django-workshop</id>
  218. <content type="html"><![CDATA[<p>I helped host a free Test-Driven Django Web Development workshop on <time date="2013-11-02">Saturday November 2</time> with <a href="http://pythonsd.org/">San Diego Python</a>. We created a series of tutorials demonstrating how to create a Django-powered blog while practicing test-driven development. The <a href="http://python.org/psf/">Python Software Foundation</a> sponsored the event and the <a href="http://aicenterca.com/">Ansir Innovation Center</a> provided a venue.</p>
  219. <p>You can find the tutorials at <a href="http://bit.ly/pysd-tdd">http://bit.ly/pysd-tdd</a> . The tutorials are provided under a <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC BY-SA license</a> so you can reuse and modify them for your own purposes.</p>
  220. <p>Tutorial markdown files and working source code may be found on <a href="https://github.com/pythonsd/test-driven-django-development">Github</a>. We plan to improve and extend these tutorials for a future workshop. If you have ideas for improvements/additions or if you notice a bug, please submit an issue or open a pull request.</p>
  221. ]]></content>
  222. </entry>
  223. <entry>
  224. <title type="html"><![CDATA[Visual Integration Tests for Django]]></title>
  225. <link href="http://treyhunner.com/2013/10/visual-integration-tests-for-django/"/>
  226. <updated>2013-10-03T15:19:00-07:00</updated>
  227. <id>http://treyhunner.com/2013/10/visual-integration-tests-for-django</id>
  228. <content type="html"><![CDATA[<p>I recently added a new type of test to my testing arsenal: visual tests. Visual tests ensure the CSS, markup, and JavaScript produce a webpage that looks right.</p>
  229. <h2>Visual testing frameworks</h2>
  230. <p>Visual testing tools compare screenshots to ensure tested webpages look pixel perfect. Capturing webpage screenshots requires a full-featured web browser to render CSS and execute JavaScript. All three of the visual testing tools I found rely on Selenium or PhantomJS for rendering.</p>
  231. <h3>PhantomCSS</h3>
  232. <p><a href="https://github.com/Huddle/PhantomCSS">PhantomCSS</a> uses PhantomJS for screenshot differencing. PhantomCSS won&rsquo;t integrate directly with the Django live server or your Python test suite, so if you want to run a visual integration test, you&rsquo;d need to manually start and stop the test server between tests. I might eventually try out PhantomCSS for CSS unit tests, but I wanted to visually test my full website so I needed integration with the Django live server.</p>
  233. <h3>Django-casper</h3>
  234. <p><a href="https://github.com/dobarkod/django-casper">Django-casper</a> uses Django live server tests to execute CasperJS test files (which use PhantomJS) to compare screenshots. Each test requires an additional Python test which references a JavaScript file that executes the navigation and screenshotting code. I found this approach messy and difficult to setup.</p>
  235. <h3>Needle</h3>
  236. <p>The <a href="https://github.com/bfirsh/needle">needle</a> Python library uses Selenium to navigate your website and screenshot rendered pages. Unfortunately needle has poor test coverage, a seemingly failing test suite, and no change log. Despite these shortcomings, I went with needle for my visual integration tests because it got the job done.</p>
  237. <h2>Django and Needle</h2>
  238. <p>I used the following mixin to integrate the Django live server with needle. I used PhantomJS, but Firefox or another Selenium web driver should work as well.</p>
  239. <figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  240. <span class='line-number'>2</span>
  241. <span class='line-number'>3</span>
  242. <span class='line-number'>4</span>
  243. <span class='line-number'>5</span>
  244. <span class='line-number'>6</span>
  245. <span class='line-number'>7</span>
  246. <span class='line-number'>8</span>
  247. <span class='line-number'>9</span>
  248. <span class='line-number'>10</span>
  249. <span class='line-number'>11</span>
  250. <span class='line-number'>12</span>
  251. <span class='line-number'>13</span>
  252. <span class='line-number'>14</span>
  253. </pre></td><td class='code'><pre><code class='python'><span class='line'><span class="kn">from</span> <span class="nn">django.test</span> <span class="kn">import</span> <span class="n">LiveServerTestCase</span>
  254. </span><span class='line'><span class="kn">from</span> <span class="nn">needle.cases</span> <span class="kn">import</span> <span class="n">NeedleTestCase</span>
  255. </span><span class='line'><span class="kn">from</span> <span class="nn">selenium.webdriver</span> <span class="kn">import</span> <span class="n">PhantomJS</span>
  256. </span><span class='line'>
  257. </span><span class='line'>
  258. </span><span class='line'><span class="k">class</span> <span class="nc">DjangoNeedleTestCase</span><span class="p">(</span><span class="n">NeedleTestCase</span><span class="p">,</span> <span class="n">LiveServerTestCase</span><span class="p">):</span>
  259. </span><span class='line'>
  260. </span><span class='line'> <span class="sd">&quot;&quot;&quot;Needle test case for use with Django live server&quot;&quot;&quot;</span>
  261. </span><span class='line'>
  262. </span><span class='line'> <span class="n">driver</span> <span class="o">=</span> <span class="n">PhantomJS</span>
  263. </span><span class='line'>
  264. </span><span class='line'> <span class="nd">@classmethod</span>
  265. </span><span class='line'> <span class="k">def</span> <span class="nf">get_web_driver</span><span class="p">(</span><span class="n">cls</span><span class="p">):</span>
  266. </span><span class='line'> <span class="k">return</span> <span class="nb">type</span><span class="p">(</span><span class="s">&#39;NeedleWebDriver&#39;</span><span class="p">,</span> <span class="p">(</span><span class="n">NeedleWebDriverMixin</span><span class="p">,</span> <span class="n">cls</span><span class="o">.</span><span class="n">driver</span><span class="p">),</span> <span class="p">{})()</span>
  267. </span></code></pre></td></tr></table></div></figure>
  268. <p>Unfortunately the above code only works with the version of needle on Github. The PyPI version does not yet include the <code>NeedleWebDriverMixin</code> (which I contributed recently for Django support). I have created <a href="https://github.com/bfirsh/needle/issues/13">an issue</a> suggesting a new PyPI release be made to resolve this problem.</p>
  269. <h2>Room for improvement</h2>
  270. <p>Currently I only run my visual tests manually. Visual tests are very brittle and occasionally they just break without any changes. If I manage to stabilize my visual tests so that they pass consistently on different platforms, I may run them during continuous integration.</p>
  271. <p>Do you have another solution for visual integration testing? Let me know in the comments.</p>
  272. ]]></content>
  273. </entry>
  274. <entry>
  275. <title type="html"><![CDATA[Extensible JSON Encoder Using Single-dispatch Functions]]></title>
  276. <link href="http://treyhunner.com/2013/09/singledispatch-json-serializer/"/>
  277. <updated>2013-09-27T00:00:00-07:00</updated>
  278. <id>http://treyhunner.com/2013/09/singledispatch-json-serializer</id>
  279. <content type="html"><![CDATA[<p>Single-dispatch generic functions will be added to Python 3.4 (as proposed in <a href="http://www.python.org/dev/peps/pep-0443/">PEP 443</a>). When reading about single-dispatch functions I immediately thought of the difficulties I&rsquo;ve had with custom JSON encoders. Below I explain why custom JSON encoders can complicate your application and how single-dispatch functions could be used to create a simpler JSON encoder.</p>
  280. <h2>JSON Encoding using the json library</h2>
  281. <p>With the current Python <code>json</code> library, using an extensible JSON encoder in your generic application may require some/all of the following:</p>
  282. <ul>
  283. <li>Allowing specification of custom encoder class by client applications</li>
  284. <li>Overriding the default JSON encoder class (or a client-specified one) for any further extensions</li>
  285. <li>Passing JSON encoder classes into other serialization libraries used by your application</li>
  286. </ul>
  287. <h3>Combining JSON encoders</h3>
  288. <p>If you need to compose two custom JSON encoders specified in two different packages, you may need to:</p>
  289. <ul>
  290. <li>Use multiple inheritance and hope the encoders play nicely together</li>
  291. <li>Duplicate code from one of the packages and create a new serializer with single inheritance</li>
  292. <li>Monkey patch one or both of the libraries</li>
  293. </ul>
  294. <h2>JSON encoder using single-dispatch generic functions</h2>
  295. <p>I created a wrapper around the <code>json</code> library to make a JSON encoder using single-dispatch generic functions. Here&rsquo;s how to use it:</p>
  296. <div><script src='https://gist.github.com/6734816.js?file=example.py'></script>
  297. <noscript><pre><code>from decimal import Decimal
  298. from json_singledispatch import encode
  299. @encode.register(set)
  300. def encode_set(obj):
  301. return encode(list(obj))
  302. @encode.register(Decimal)
  303. def encode_dict(obj):
  304. return encode(str(obj))
  305. print encode({'key': &quot;value&quot;})
  306. print encode({5, 6})
  307. print encode(Decimal(&quot;5.6&quot;))</code></pre></noscript></div>
  308. <p>As you can see, it&rsquo;s fairly easy to extend the encoder to understand serialization rules for new data types.</p>
  309. <p>The impementation is fairly simple, albeit a hack:</p>
  310. <div><script src='https://gist.github.com/6734816.js?file=json_singledispatch.py'></script>
  311. <noscript><pre><code>import json
  312. from singledispatch import singledispatch
  313. class _CustomEncoder(json.JSONEncoder):
  314. def default(self, obj):
  315. for obj_type, handler in encode.registry.items():
  316. if isinstance(obj, obj_type) and obj_type is not object:
  317. return handler(obj)
  318. return super(_CustomEncoder, self).default(obj)
  319. @singledispatch
  320. def encode(obj, **kwargs):
  321. return json.dumps(obj, cls=_CustomEncoder, **kwargs)</code></pre></noscript></div>
  322. <p>This code is intended as a proof-of-concept to demonstrate the power of single-dispatch generic functions. Feel free to use it however you like.</p>
  323. <h2>Related Links</h2>
  324. <ul>
  325. <li><a href="http://lukasz.langa.pl/8/single-dispatch-generic-functions/">What single-dispatch generic functios mean for you</a></li>
  326. <li><a href="http://julien.danjou.info/blog/2013/python-3.4-single-dispatch-generic-function">Python 3.4 single dispatch, a step into generic functions</a></li>
  327. </ul>
  328. ]]></content>
  329. </entry>
  330. <entry>
  331. <title type="html"><![CDATA[Test-Inspired Development]]></title>
  332. <link href="http://treyhunner.com/2013/07/test-inspired-development/"/>
  333. <updated>2013-07-28T00:00:00-07:00</updated>
  334. <id>http://treyhunner.com/2013/07/test-inspired-development</id>
  335. <content type="html"><![CDATA[<p>You need to write tests for your code to demonstrate that it works as expected. In this post I will note the method I usually use for writing code and writing tests.</p>
  336. <p>I follow a method of development similar to test-driven development, which I call &ldquo;test-inspired development&rdquo;. Here&rsquo;s roughly what I do:</p>
  337. <ol>
  338. <li>Write a bit of code</li>
  339. <li>Write a bit of a unit test for your code</li>
  340. <li>Ensure the test succeeds, but fails when the code is commented out or git stash-ed</li>
  341. <li>Repeat steps 1, 2, and 3 as needed</li>
  342. <li>Start writing an integration test, fleshing out as far as you can</li>
  343. <li>Make sure the integration test fails at the correct point</li>
  344. <li>Repeat steps 5 and 6 as needed</li>
  345. <li>Make sure no commit you make along the way contains both tests and code</li>
  346. <li>Use git rebase if necessary to ensure the test commits come before the code commits</li>
  347. </ol>
  348. <p>Ideally I would write all of my test code first and then write my application code and ensure my tests pass. That isn&rsquo;t what usually happens though. I often find it tricky to write full tests before writing any code. Even when writing code in small parts, sometimes my code gets written before the corresponding test code.</p>
  349. <p>I write all of my code in feature branches (short-lived branches forked from master) and I often create a pull request (even against my own repository) when creating a new feature. Because I commit the tests first, a code reviewer can easily confirm that all the new tests actually fail correctly without the code present by simply rolling back to the last test commit and running the tests.</p>
  350. <p>I usually write my unit tests first and at or near the same time as my code because they help often help shape my code heavily. The functional/integration tests are important for ensuring that the application actually works all the way through (working parts are useless on their own).</p>
  351. <p>Have questions about my testing methodology? Do you use a different technique? Feel free to share your thoughts in the comments below.</p>
  352. ]]></content>
  353. </entry>
  354. <entry>
  355. <title type="html"><![CDATA[Maintaining an Open Source Project]]></title>
  356. <link href="http://treyhunner.com/2013/07/maintaining-an-open-source-project/"/>
  357. <updated>2013-07-21T00:00:00-07:00</updated>
  358. <id>http://treyhunner.com/2013/07/maintaining-an-open-source-project</id>
  359. <content type="html"><![CDATA[<p>This post contains some of my opinions on how to maintain an open source project well. I&rsquo;ve built up my opinions on this topic from maintaining and contributing to open source projects. Keep in mind that I am not an expert on this topic.</p>
  360. <h2>Structure your project logically</h2>
  361. <p>You shouldn&rsquo;t need to worry about structuring your project because others have done that work for you. <strong>Base your project&rsquo;s design on other well-organized projects.</strong></p>
  362. <p>I compiled some tips I try to follow for open source Django projects I maintain in my <a href="https://github.com/treyhunner/django-skeleton-app">django-skeleton-app</a> repository. Check it out if you&rsquo;re starting a new Python project.</p>
  363. <h2>Make it very easy to contribute</h2>
  364. <p>Document your process for contributing code, filing bugs, and maintaining other feedback. Reference this process in your README file, your documentation, and your <a href="https://github.com/blog/1184-contributing-guidelines">CONTRIBUTING</a> file. If you need a place to discuss project development outside of the issue tracker, consider creating a mailing list.</p>
  365. <h2>Be kind</h2>
  366. <p>Thank your contributors, appologize if you make a big mistake or say something rude, and be very courteous. Add all your contributors to the AUTHORS file and grant push access to active contributors.</p>
  367. <h2>Be Responsive</h2>
  368. <p>Don&rsquo;t have time to look over a new pull request this week? Make a comment noting when you&rsquo;ll give your feedback. This way your contributor can add a friendly reminder later without feeling rude.</p>
  369. <p>Realized you forgot to respond to a 12 month old pull request? Respond right now and ask whether the code or problem is still relevant. A late response is better than none at all.</p>
  370. <p>Don&rsquo;t know if you&rsquo;ll ever have time to look at an issue? Say so! <strong>Do not leave your contributors in the dark!</strong></p>
  371. <h2>Automate all the things</h2>
  372. <p>Use:</p>
  373. <ul>
  374. <li><a href="https://travis-ci.org/">Travis CI</a>: run continuous integration tests for all pushes and pull requests</li>
  375. <li><a href="https://coveralls.io/">Coveralls</a>: measure code coverage while running your CI tests</li>
  376. <li>A documentation site like <a href="https://readthedocs.org/">Read The Docs</a> which auto-updates on pushes</li>
  377. </ul>
  378. <h2>Learn from others</h2>
  379. <p>I&rsquo;m not the only one with opinions about open source.</p>
  380. <p>Read more opinions on maintaining an open source project:</p>
  381. <ul>
  382. <li><a href="https://segment.io/blog/tips-for-maintaining-an-open-source-library/">Tips for maintaining an Open-Source library</a></li>
  383. <li><a href="http://www.codesimplicity.com/post/open-source-community-simplified/">Open Source Community, Simplified</a></li>
  384. <li><a href="https://hacks.mozilla.org/2013/05/how-to-spread-the-word-about-your-code/">How to Spead The Word About Your Code</a></li>
  385. </ul>
  386. <p>Watch some videos about maintaining an open source project:</p>
  387. <ul>
  388. <li><a href="http://www.youtube.com/watch?v=xgWFTrXn0_U">Maintaining Your Sanity While Maintaining Your Open Source App</a></li>
  389. <li><a href="http://pyvideo.org/video/1795/write-the-docs">Write the Docs</a></li>
  390. </ul>
  391. ]]></content>
  392. </entry>
  393. <entry>
  394. <title type="html"><![CDATA[Log All Outgoing Emails in Django]]></title>
  395. <link href="http://treyhunner.com/2013/05/django-email-log/"/>
  396. <updated>2013-05-20T00:00:00-07:00</updated>
  397. <id>http://treyhunner.com/2013/05/django-email-log</id>
  398. <content type="html"><![CDATA[<p>Ever needed to determine whether an email was sent from a Django project? I
  399. made a Django application that does exactly that: <a href="https://github.com/treyhunner/django-email-log">django-email-log</a>.</p>
  400. <p>I got the idea from <a href="http://stackoverflow.com/a/7553759/98187">a StackOverflow answer</a> and I decided to make a real
  401. application out of it. All emails are stored in a single model which can
  402. easily be viewed, searched, sorted, and filtered from the admin site.</p>
  403. <p>I used test-driven development when making the app and I baked in Python 3
  404. support from the beginning. I found the process of TDD for a standalone
  405. Python package fairly easy and enjoyable.</p>
  406. ]]></content>
  407. </entry>
  408. <entry>
  409. <title type="html"><![CDATA[Django-simple-history Is Back]]></title>
  410. <link href="http://treyhunner.com/2013/05/django-simple-history/"/>
  411. <updated>2013-05-01T00:00:00-07:00</updated>
  412. <id>http://treyhunner.com/2013/05/django-simple-history</id>
  413. <content type="html"><![CDATA[<p>I wrote <a href="http://treyhunner.com/2011/09/django-and-model-history/">a post</a> over a year ago about recording a history of changes for Django model instances. I evaluated three different Django packages to record model history. My favorite of the options, django-simple-history, was abandoned and development continued through multiple forks.</p>
  414. <p>I recently attempted to revive <a href="https://github.com/treyhunner/django-simple-history">django-simple-history</a>. I added tests, put it <a href="https://pypi.python.org/pypi/django-simple-history/">on PyPI</a>, and made it easier to use with newer versions of Django. I moved my fork of the project to git and Github, added Travis and Coveralls support for continuous integration and code coverage tracking, and noted future features on the issue tracker.</p>
  415. <p>Soon after I started writing tests for the project I received feature requests, issues, pull requests, and emails with words of encouragement. I appreciate all of the help I&rsquo;ve had while reviving the project. I plan to remain responsive to the suggestions for my fork of the code. If you&rsquo;d like to help out with the project please feel free to submit an issue, make a pull request, or comment on the code commits on the <a href="https://github.com/treyhunner/django-simple-history">Github page</a>.</p>
  416. ]]></content>
  417. </entry>
  418. <entry>
  419. <title type="html"><![CDATA[Random Name Generator Website]]></title>
  420. <link href="http://treyhunner.com/2013/03/pseudorandom.name/"/>
  421. <updated>2013-03-10T00:00:00-08:00</updated>
  422. <id>http://treyhunner.com/2013/03/pseudorandom.name</id>
  423. <content type="html"><![CDATA[<p>In my <a href="http://treyhunner.com/2013/02/random-name-generator/">last post</a> I discussed the <strong>names</strong> Python library that generates
  424. random names. I&rsquo;ve now used this Python library to make a basic Flask website
  425. that generates random names. You can visit it at:
  426. <a href="http://www.pseudorandom.name">http://www.pseudorandom.name</a></p>
  427. <h2>It&rsquo;s responsive</h2>
  428. <p>The font size and margin on pseudorandom.name change based on the web browser
  429. width and height. I determined the font sizes I wanted to use for each screen
  430. width by using the longest first and last name in the name files I&rsquo;m using (11
  431. and 13 characters respectively). The website looks reasonable on various
  432. desktop resolutions and on phone screens.</p>
  433. <h2>It&rsquo;s an HTTP API</h2>
  434. <p>If curl is used to access the site, plain text is returned instead of an HTML
  435. webpage. For example:</p>
  436. <pre><code>curl www.pseudorandom.name
  437. </code></pre>
  438. <p>The site is hosted on Heroku which <a href="https://devcenter.heroku.com/articles/custom-domains#apex-domains-examplecom">doesn&rsquo;t support bare domains</a>.
  439. Currently <a href="http://pseudorandom.name">http://pseudorandom.name</a> redirects to <a href="http://www.pseudorandom.name">http://www.pseudorandom.name</a> so
  440. using the bare domain requires telling curl to follow redirects with <code>-L</code>:</p>
  441. <pre><code>curl -L pseudorandom.name
  442. </code></pre>
  443. <h2>More to come?</h2>
  444. <p>It might be nice to allow generating multiple names at once, generating
  445. gender-specific names, and maybe providing other content types (JSON). The
  446. HTML version could also use a cleaner, more feature-full design.</p>
  447. <p>Also it would probably be more efficient to use a SQL database for querying
  448. random names so I may eventually abandon the names library and use a database
  449. for querying.</p>
  450. <p>The website&rsquo;s source code is <a href="https://github.com/treyhunner/pseudorandom.name">hosted on Github</a> and provided under an
  451. <a href="http://th.mit-license.org/2013">MIT license</a>. Feel free to fork it or submit an issue.</p>
  452. ]]></content>
  453. </entry>
  454. <entry>
  455. <title type="html"><![CDATA[Random Name Generator in Python]]></title>
  456. <link href="http://treyhunner.com/2013/02/random-name-generator/"/>
  457. <updated>2013-02-17T00:00:00-08:00</updated>
  458. <id>http://treyhunner.com/2013/02/random-name-generator</id>
  459. <content type="html"><![CDATA[<p>I&rsquo;ve used multiple websites to generate random names for my test data when
  460. running manual or automated QA tests.</p>
  461. <p>Since discovering DuckDuckGo&rsquo;s <a href="https://duckduckgo.com/?q=random+word">random word</a> generation, I&rsquo;ve
  462. <a href="https://duckduckhack.uservoice.com/forums/5168-ideas-for-duckduckgo-instant-answer-plugins/suggestions/2850418-random-name">hoped</a> that someone would make a DuckDuckGo plugin to generate
  463. random names also.</p>
  464. <p>I didn&rsquo;t want to write my own DuckDuckGo plugin yet so I made a Python-powered
  465. command line tool instead using <a href="https://www.census.gov/genealogy/www/data/1990surnames/index.html">1990 Census data</a>.</p>
  466. <p>The program is called <code>names</code> and can be found on <a href="https://github.com/treyhunner/names">Github</a> and <a href="http://pypi.python.org/pypi/names/0.1">PyPI</a>.</p>
  467. <h3>It&rsquo;s really simple</h3>
  468. <p>It&rsquo;s basically just one file currently that&rsquo;s <a href="https://github.com/treyhunner/names/blob/f99542dc21f48aa82da4406f8ce408e92639430d/names/__init__.py">about 40 lines long</a>.
  469. There is only one feature available from the command line currently: generate a
  470. single random full name. There&rsquo;s a few more features if importing as a Python
  471. package: generate random last name or generate random first name (with or
  472. without specifying gender), generate random full name (also without or without
  473. gender).</p>
  474. <p>The random name picker relies on the cumulative frequencies listed in the
  475. included Census data files. Here&rsquo;s the steps that are taken:
  476. 1. A random floating point number is chosen between 0.0 and 90.0
  477. 2. Name file lines are iterated through until a cumulative frequency is found
  478. that is less than the randomly generated number
  479. 3. The name on that line is chosen and returned (or printed out)</p>
  480. <h3>Examples</h3>
  481. <p>Here&rsquo;s how you use it from the command line:</p>
  482. <pre><code>$ names
  483. Kara Lopes
  484. </code></pre>
  485. <p>Here&rsquo;s how you use it as a Python package:</p>
  486. <pre><code>&gt;&gt;&gt; import names
  487. &gt;&gt;&gt; names.get_full_name()
  488. u'Patricia Halford'
  489. &gt;&gt;&gt; names.get_full_name(gender='male')
  490. u'Patrick Keating'
  491. &gt;&gt;&gt; names.get_first_name()
  492. 'Bernard'
  493. &gt;&gt;&gt; names.get_first_name(gender='female')
  494. 'Christina'
  495. &gt;&gt;&gt; names.get_last_name()
  496. 'Szczepanek'
  497. </code></pre>
  498. ]]></content>
  499. </entry>
  500. <entry>
  501. <title type="html"><![CDATA[Tmuxstart]]></title>
  502. <link href="http://treyhunner.com/2012/12/tmuxstart/"/>
  503. <updated>2012-12-15T00:00:00-08:00</updated>
  504. <id>http://treyhunner.com/2012/12/tmuxstart</id>
  505. <content type="html"><![CDATA[<p>I&rsquo;ve been using a helper script to manage all of my tmux sessions for the last
  506. few months (nearly since the time I switched from screen to tmux).</p>
  507. <h3>A tmuxinator alternative</h3>
  508. <p>I use a separate session for each project I work on and I wanted a way to
  509. easily connect to project sessions with the same default windows and
  510. environment variables each time. I tried using <a href="https://github.com/aziz/tmuxinator">tmuxinator</a>, but I found it
  511. to be too complicated for my needs. I had trouble working within the
  512. limitations of the yaml files and I found the installation to be overly
  513. complicated (especially when working on a new server that didn&rsquo;t have ruby
  514. installed). After I realized how simple it would be to solve my own problem I
  515. made the tmuxstart script.</p>
  516. <h3>Now available on Github</h3>
  517. <p>While updating my <a href="https://github.com/treyhunner/dotfiles">dotfiles</a> repository I realized I hadn&rsquo;t yet incorporated
  518. my tmuxstart workflow into my <code>.tmux.conf</code> file. Yesterday I decided to make
  519. README and LICENSE files for my script and put it under version control. I
  520. also added some helper functions to make the script even easier to use. It&rsquo;s
  521. now available in my <a href="https://github.com/treyhunner/tmuxstart">tmuxstart</a> repository on Github.</p>
  522. <h3>Example usage</h3>
  523. <p>Below is an example of how to use tmuxstart.</p>
  524. <ol>
  525. <li>Add the following line to your <code>~/.tmux.conf</code> file:</li>
  526. </ol>
  527. <figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  528. </pre></td><td class='code'><pre><code class=''><span class='line'>bind S command-prompt -p "Make/attach session:" "new-window 'tmuxstart \'%%\''"</span></code></pre></td></tr></table></div></figure>
  529. <ol>
  530. <li>Create a <code>~/.tmuxstart</code> directory:</li>
  531. </ol>
  532. <figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  533. </pre></td><td class='code'><pre><code class=''><span class='line'>mkdir ~/.tmuxstart</span></code></pre></td></tr></table></div></figure>
  534. <ol>
  535. <li>Create a file <code>~/.tmuxstart/main</code> with the following contents:</li>
  536. </ol>
  537. <figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  538. <span class='line-number'>2</span>
  539. </pre></td><td class='code'><pre><code class=''><span class='line'>new_session htop
  540. </span><span class='line'>new_window</span></code></pre></td></tr></table></div></figure>
  541. <p>Now you can create a tmux session called &ldquo;main&rdquo; with <a href="http://htop.sourceforge.net/">htop</a> in the first
  542. window and a shell in the second window by executing:</p>
  543. <pre><code>tmuxstart main
  544. </code></pre>
  545. <p>Or from an existing tmux server type <code>&lt;PREFIX&gt; S</code> (<code>&lt;PREFIX&gt;</code> is Ctrl-B by
  546. default), type <code>main</code> and hit Enter.</p>
  547. <p>When using either of these methods if a session called &ldquo;main&rdquo; already exists
  548. the existing session will be used instead.</p>
  549. <h3>How I use this</h3>
  550. <p>I have a separate tmuxstart session file for each of my Django projects and
  551. some of my open source projects also. I use a &ldquo;main&rdquo; session file similar to
  552. the one above and I have Gnome Terminal set to start with the custom command
  553. &ldquo;tmuxstart main&rdquo; instead of a shell.</p>
  554. ]]></content>
  555. </entry>
  556. <entry>
  557. <title type="html"><![CDATA[Maintaining Consistent Coding Conventions With EditorConfig]]></title>
  558. <link href="http://treyhunner.com/2012/02/editorconfig/"/>
  559. <updated>2012-02-17T00:00:00-08:00</updated>
  560. <id>http://treyhunner.com/2012/02/editorconfig</id>
  561. <content type="html"><![CDATA[<p>I often have indentation issues when coding because I work with many open
  562. source (and closed source) projects that have different identation styles. For
  563. example, my <a href="https://github.com/treyhunner/jquery-formrestrict">jQuery formrestrict</a> uses 4 spaces for indentation,
  564. <a href="https://github.com/kswedberg/jquery-expander">jQuery expander</a> uses 2 spaces, and <a href="https://github.com/gbirke/jquery_pagination">jQuery pagination</a> uses hard tabs.
  565. I don&rsquo;t want to change my text editor&rsquo;s indentation settings every time I open
  566. one of these files and using a plugin to auto-detect indentation is only a
  567. partial solution. To solve this problem I started a project I call
  568. EditorConfig.</p>
  569. <p><a href="http://editorconfig.org">EditorConfig</a> defines coding conventions for groups of files and instructs
  570. text editors to adhere to these conventions. To solve my problem, I just
  571. create a file called <code>.editorconfig</code> that sets indentation for my files and
  572. with my EditorConfig plugin installed Vim takes care of the rest. Here&rsquo;s an
  573. example <code>.editorconfig</code> file I could add to my project that uses
  574. <a href="https://github.com/kswedberg/jquery-expander">jQuery expander</a> and <a href="https://github.com/gbirke/jquery_pagination">jQuery pagination</a>:</p>
  575. <figure class='code'><figcaption><span>.editorconfig</span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
  576. <span class='line-number'>2</span>
  577. <span class='line-number'>3</span>
  578. <span class='line-number'>4</span>
  579. <span class='line-number'>5</span>
  580. <span class='line-number'>6</span>
  581. <span class='line-number'>7</span>
  582. <span class='line-number'>8</span>
  583. <span class='line-number'>9</span>
  584. <span class='line-number'>10</span>
  585. <span class='line-number'>11</span>
  586. </pre></td><td class='code'><pre><code class='ini'><span class='line'><span class="k">[*.js]</span>
  587. </span><span class='line'><span class="na">indent_style</span> <span class="o">=</span> <span class="s">space</span>
  588. </span><span class='line'><span class="na">indent_size</span> <span class="o">=</span> <span class="s">4</span>
  589. </span><span class='line'>
  590. </span><span class='line'><span class="k">[jquery.expander.js]</span>
  591. </span><span class='line'><span class="na">indent_style</span> <span class="o">=</span> <span class="s">space</span>
  592. </span><span class='line'><span class="na">indent_size</span> <span class="o">=</span> <span class="s">2</span>
  593. </span><span class='line'>
  594. </span><span class='line'><span class="k">[jquery.pagination.js]</span>
  595. </span><span class='line'><span class="na">indent_style</span> <span class="o">=</span> <span class="s">tab</span>
  596. </span><span class='line'><span class="na">indent_size</span> <span class="o">=</span> <span class="s">4</span>
  597. </span></code></pre></td></tr></table></div></figure>
  598. <p>With this <code>.editorconfig</code> file, all JavaScript files use 4 spaces for
  599. indentation except for <code>jquery.expander.js</code> which uses 2 spaces and
  600. <code>jquery.pagination.js</code> which uses hard tabs with a column width of 4. If I put
  601. my <code>.editorconfig</code> file under version control, other developers working on my
  602. project can see the coding conventions I defined and if their text editor has
  603. an EditorConfig plugin installed their editor will automatically use the
  604. correct indentation as well.</p>
  605. <p>Example EditorConfig files can be seen in my own projects (such as
  606. <a href="https://github.com/treyhunner/dotfiles/blob/master/.editorconfig">in my dotfiles repo</a>) and in the
  607. <a href="https://github.com/treyhunner/dotfiles/blob/master/.editorconfig">various EditorConfig plugin codebases</a>. More
  608. information on EditorConfig can be found at the
  609. <a href="http://editorconfig.org">EditorConfig website</a>.</p>
  610. <p>EditorConfig plugins are <a href="http://editorconfig.org/#download">available for 6 different editors</a>
  611. now. If you like the idea, <a href="http://editorconfig.org">try out EditorConfig</a> for your
  612. favorite editor and <a href="https://groups.google.com/forum/?fromgroups#!forum/editorconfig">tell us what you think</a>. If you don&rsquo;t like
  613. the idea or have a problem with EditorConfig please <a href="https://github.com/editorconfig/editorconfig/issues">submit an issue</a>,
  614. add a thread to <a href="https://groups.google.com/forum/?fromgroups#!forum/editorconfig">the mailing list</a>, or send me an email voicing
  615. your concern.</p>
  616. ]]></content>
  617. </entry>
  618. <entry>
  619. <title type="html"><![CDATA[Migrating From Subversion to Git]]></title>
  620. <link href="http://treyhunner.com/2011/11/migrating-from-subversion-to-git/"/>
  621. <updated>2011-11-17T00:00:00-08:00</updated>
  622. <id>http://treyhunner.com/2011/11/migrating-from-subversion-to-git</id>
  623. <content type="html"><![CDATA[<p>I recently migrated multiple Subversion repositories to Git. I found
  624. <a href="http://blog.woobling.org/2009/06/git-svn-abandon.html" title="Migrating from Subversion to Git">this blog post</a> very helpful during the process. Here are
  625. some tips I found helpful that were not mentioned in that post.</p>
  626. <h3>Generating an authors file</h3>
  627. <p>Subversion denotes authors by usernames while Git uses name and email. An
  628. authors file can be used to map subversion usernames to Git authors when
  629. creating the Git repository, like so:</p>
  630. <pre><code>git-svn clone --stdlayout --authors-file=authors.txt http://your/svn/repo/url
  631. </code></pre>
  632. <p>The trickest part about <a href="http://triptico.com/notes/8d4510bb.html">making an authors file</a> was finding
  633. all the authors. I found this command useful for finding usernames of all
  634. Subversion committers:</p>
  635. <pre><code>svn log | awk '($0 ~ /^r/) {print $3}' | sort -u
  636. </code></pre>
  637. <p>A similar method of creating an authors file is
  638. <a href="http://technicalpickles.com/posts/creating-a-svn-authorsfile-when-migrating-from-subversion-to-git/">presented here</a>.</p>
  639. <h3>Removing git-svn-id messages</h3>
  640. <p>When migrating from Subversion, every commit messages has a <code>git-svn-id</code> line
  641. appended to it like this one:</p>
  642. <blockquote><p>git-svn-id: <a href="http://svn/repo/url/trunk@9837">http://svn/repo/url/trunk@9837</a> 1eab27b1-3bc6-4acd-4026-59d9a2a3569e</p></blockquote>
  643. <p>If you are planning on migrating away from your old Subversion repository
  644. entirely, there&rsquo;s no need to keep these. The following command (taken from the
  645. <a href="http://linux.die.net/man/1/git-filter-branch">git filter-branch man page</a>) removes these <code>git-svn-id</code>
  646. lines from all commit messages in the current branch:</p>
  647. <pre><code>git filter-branch -f --msg-filter 'sed -e "/git-svn-id:/d"'
  648. </code></pre>
  649. <h3>Removing empty commit messages</h3>
  650. <p>Subversion allows empty commit messages, but Git does not. Any empty commit
  651. messages in your newly migrated git repository should be replaced so commands
  652. like <code>git rebase</code> will work on these commits.</p>
  653. <p>This command will replace all empty commit messages with <nobr>
  654. &ldquo;&lt;empty commit message&gt;&rdquo;</nobr>:</p>
  655. <pre><code>git filter-branch -f --msg-filter '
  656. read msg
  657. if [ -n "$msg" ] ; then
  658. echo "$msg"
  659. else
  660. echo "&lt;empty commit message&gt;"
  661. fi'
  662. </code></pre>
  663. <h3>Poking around</h3>
  664. <p>After you&rsquo;ve cleaned up your new <em>master</em> branch, you should cleanup other
  665. branches you plan to keep. Just <code>git checkout</code> each branch and repeat the same
  666. steps. To find the remote subversion branches available for checkout use
  667. <code>git branch -a</code>.</p>
  668. <p>Migrating between version control systems may be a good time to permanently
  669. cleanup commit history with a <code>git rebase</code> or eliminate large files
  670. that were never used with a <code>git filter-branch</code>. Just remember to make backups of
  671. previous working versions of branches before changing them, just in case.</p>
  672. ]]></content>
  673. </entry>
  674. <entry>
  675. <title type="html"><![CDATA[Django and Model History]]></title>
  676. <link href="http://treyhunner.com/2011/09/django-and-model-history/"/>
  677. <updated>2011-09-29T00:00:00-07:00</updated>
  678. <id>http://treyhunner.com/2011/09/django-and-model-history</id>
  679. <content type="html"><![CDATA[<p><strong>May 2013 Update:</strong> <a href="http://treyhunner.com/2013/05/django-simple-history/">django-simple-history is back</a></p>
  680. <p>Recently I had a need to store a snapshot of every state of particular model instance in a Django application. I basically needed version control for the rows in my database tables. When searching for applications that provided this feature, which I call <strong>model history</strong>, I found <a href="http://djangopackages.com/grids/g/model-audit/">many different approaches</a> but few good comparisons of them.  In an attempt to fill that void, I&rsquo;m going to detail some of my findings here.</p>
  681. <h3>django-reversion</h3>
  682. <p>The <a href="https://github.com/etianen/django-reversion">django-reversion</a> application was started in 2008 by Dave Hall. Reversion uses only one table to store data for all version-tracked models. Each version of a model adds a new row to this table, using a JSON object representing the model state. Models can be reverted to previous versions from the admin interface.  This single-table version structure makes django-reversion very easy to install and to uninstall, but it also creates <a href="http://groups.google.com/group/django-reversion/browse_thread/thread/922b4e42d9577e0b">problems when model fields are changed</a>.</p>
  683. <h3>django-revisions</h3>
  684. <p>The <a href="https://github.com/stdbrouw/django-revisions">django-revisions</a> application was created by Stijn Debrouwere in 2010 because the existing Django model history applications at the time were abandoned or suffered from fundamental design problems. Revisions uses a model history method called same-table versioning (<a href="http://stdbrouw.github.com/django-revisions/design.html">design details outlined here</a>). Same-table versioning adds a few fields to each version-tracked model which allows it to record the most recent version of each model as well as old versions in the original model table. Model changes are simplified because they change all versions at once and no new tables need to be added to use revisions (just new fields on existing tables). The only problem I found with revisions was that it does not currently support database-level uniqueness constraints. Adding <code>unique=True</code> to a model field or a <code>unique_together</code> Meta attribute will result in an error. Currently uniqueness constraints must be specified in a separate way for Revisions to honor them when saving models.</p>
  685. <h3>django-simple-history</h3>
  686. <p>The <a href="https://bitbucket.org/q/django-simple-history/">django-simple-history</a> application was based on code originally written by Marty Alchin, author of Pro Django. Marty Alchin posted <a href="https://code.djangoproject.com/wiki/AuditTrail">AuditTrail</a> on the Django trac wiki in 2007 and later revised and republished the code in his book Pro Django in 2008, renaming it to HistoricalRecords. Corey Bertram created django-simple-history from this code and put it online in 2010.</p>
  687. <p>Simple History works by creating a separate &ldquo;historical&rdquo; model for each model that requires an audit trail and storing a snapshot of each changed model instance in that historical model. For example, a Book model would have a HistoricalBook created from it that would store a new HistoricalBook instance every time a Book instance was changed. Collisions are avoided by disabling uniqueness constraints and model schema changes are accepted by automatically changing historical models as well. This method comes at the cost of creating an extra table in the database for each model that needs history.</p>
  688. <h3>My conclusions</h3>
  689. <p>When testing these three applications myself, I immediately eliminated django-reversion because I needed to allow easy model schema changes for my project. I found that both django-revisions and django-simple-history worked well with schema migrations through <a href="http://south.aeracode.org/docs/about.html">South</a> (which I use on everything). Django-revisions worked better for data migrations in South (due to only needing to change one model), but the uniqueness constraint problems with django-revisions would have been problematic for some of my models. So eventually I settled on <a href="https://bitbucket.org/q/django-simple-history/">django-simple-history</a>.</p>
  690. ]]></content>
  691. </entry>
  692. <entry>
  693. <title type="html"><![CDATA[Sharing Screenshots in Linux]]></title>
  694. <link href="http://treyhunner.com/2011/04/sharing-screenshots-in-linux/"/>
  695. <updated>2011-04-03T00:00:00-07:00</updated>
  696. <id>http://treyhunner.com/2011/04/sharing-screenshots-in-linux</id>
  697. <content type="html"><![CDATA[<p>I have been using Github Issues recently and loving its simplicity.  Unfortunately, I&rsquo;ve found that I often need to upload screenshots to demonstrate bugs and Issues does not support file uploads.  There are <a href="http://wiki.dropbox.com/DropboxAddons/DropboxScreenGrabber">Windows</a> and <a href="http://www.getcloudapp.com/">Mac</a> applications that solve this problem by capturing a screenshot, uploading it, and copying a URL to access the screenshot to the clipboard.</p>
  698. <p>I did not find any Linux applications that will capture/upload a screenshot and copy the URL but I discovered <a href="http://forums.dropbox.com/topic.php?id=21735">a thread in the Dropbox forums</a> with a script that does just that.  I added comments to the script, changed the variable names, removed the need for a temporary file, and added a <code>notify-send</code> call as a visual cue (should work on Ubuntu).  I have the script mapped to <kbd>Ctrl-PrtScrn</kbd> in Ubuntu.</p>
  699. <div><script src='https://gist.github.com/892492.js'></script>
  700. <noscript><pre><code>#!/bin/sh
  701. # Ubuntu-specific modification of http://wiki.dropbox.com/TipsAndTricks/ShareScreenshots
  702. # Change these
  703. DB_USER_ID=YOURDBUSERID
  704. BITLY_USERNAME=YOURBITLYUSERNAME
  705. BITLY_API_KEY=YOURBITLYKEYHERE
  706. DROPBOX_PUBLIC_DIR=~/Dropbox/Public
  707. SCREENSHOT_DIR=screenshots
  708. CAPTURE_DELAY=0
  709. PICTURE_QUALITY=50
  710. FILE_EXTENSION=png
  711. TIME=$(date +%Y%m%d%H%M%S)
  712. FILENAME=$TIME.$FILE_EXTENSION
  713. # Move to the directory where screenshots will be stored
  714. mkdir -p $DROPBOX_PUBLIC_DIR/$SCREENSHOT_DIR
  715. cd $DROPBOX_PUBLIC_DIR/$SCREENSHOT_DIR
  716. # Take screenshot and save in screenshot directory
  717. scrot -d $CAPTURE_DELAY -q $PICTURE_QUALITY $FILENAME
  718. # Get Dropbox public URL for screenshot
  719. DB_URL=&quot;http://dl.dropbox.com/u/$DB_USER_ID/$SCREENSHOT_DIR/$FILENAME&quot;
  720. # Get bit.ly shortened URL for Dropbox URL
  721. ESCAPED_DB_URL=&quot;$(echo $DB_URL | sed 's,:,%3A,g;s,/,%2F,g')&quot;
  722. BITLY_API_CALL=&quot;http://api.bit.ly/v3/shorten?login=$BITLY_USERNAME&amp;apiKey=$BITLY_API_KEY&amp;longUrl=$ESCAPED_DB_URL&amp;format=txt&quot;
  723. SHORT_URL=$(curl -s -S $BITLY_API_CALL)
  724. # Copy shortened URL to clipboard
  725. echo $SHORT_URL | xclip -sel clip
  726. # Display message to user (requires libnotify)
  727. notify-send &quot;Screenshot added&quot; &quot;Screenshot link copied to clipboard: $SHORT_URL&quot;
  728. </code></pre></noscript></div>
  729. ]]></content>
  730. </entry>
  731. <entry>
  732. <title type="html"><![CDATA[Encrypted Private Keys in Django]]></title>
  733. <link href="http://treyhunner.com/2010/12/encrypted-private-keys-in-django/"/>
  734. <updated>2010-12-11T00:00:00-08:00</updated>
  735. <id>http://treyhunner.com/2010/12/encrypted-private-keys-in-django</id>
  736. <content type="html"><![CDATA[<p>Uniquely identifiable URLs are necessary for many web applications. For example, a website that provides book reviews may identify the URL of a specific book like this: <strong>www.example.com/books/8839/</strong>. The easiest way to identify entities in Django is to use the unique primary key of each object, which by default is an auto-incremented positive integer.</p>
  737. <p>Revealing the primary key of an entity is often not desirable. An astute visitor of the website mentioned above may be able to guess information from the URL such as how many book reviews are available on the website or how old specific reviews are.</p>
  738. <p>The code snippet below demonstrates one way to use a unique but cryptic identifier for an object without needing to change the way primary keys are generated. There are two notable extensions to the basic Django Model in the below code:</p>
  739. <ol>
  740. <li>The encrypted_pk and encrypted_id model properties return an AES-encrypted version of the primary key as a 13 character base-36 string.</li>
  741. <li>The get method of the default manager can be queried with an encrypted primary key by using the keyword argument encrypted_pk.</li>
  742. </ol>
  743. <p>Feel free to use this code however you want.</p>
  744. <div><script src='https://gist.github.com/735861.js'></script>
  745. <noscript><pre><code># This code is under the MIT license.
  746. # Inspired by this StackOverflow question:
  747. http://stackoverflow.com/questions/3295405/creating-django-objects-with-a-random-primary-key
  748. import struct
  749. from Crypto.Cipher import DES
  750. from django.db import models
  751. def base36encode(number):
  752. &quot;&quot;&quot;Encode number to string of alphanumeric characters (0 to z). (Code taken from Wikipedia).&quot;&quot;&quot;
  753. if not isinstance(number, (int, long)):
  754. raise TypeError('number must be an integer')
  755. if number &lt; 0:
  756. raise ValueError('number must be positive')
  757. alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'
  758. base36 = ''
  759. while number:
  760. number, i = divmod(number, 36)
  761. base36 = alphabet[i] + base36
  762. return base36 or alphabet[0]
  763. def base36decode(numstr):
  764. &quot;&quot;&quot;Convert a base-36 string (made of alphanumeric characters) to its numeric value.&quot;&quot;&quot;
  765. return int(numstr,36)
  766. class EncryptedPKModelManager(models.Manager):
  767. &quot;&quot;&quot;This manager allows models to be identified based on their encrypted_pk value.&quot;&quot;&quot;
  768. def get(self, *args, **kwargs):
  769. encrypted_pk = kwargs.pop('encrypted_pk', None)
  770. if encrypted_pk:
  771. # If found, decrypt encrypted_pk argument and set pk argument to the appropriate value
  772. kwargs['pk'] = struct.unpack('&lt;Q', self.model.encryption_obj.decrypt(
  773. struct.pack('&lt;Q', base36decode(encrypted_pk))
  774. ))[0]
  775. return super(EncryptedPKModelManager, self).get(*args, **kwargs)
  776. class EncryptedPKModel(models.Model):
  777. &quot;&quot;&quot;Adds encrypted_pk property to children which returns the encrypted value of the primary key.&quot;&quot;&quot;
  778. encryption_obj = DES.new('8charkey') # This 8 character secret key should be changed!
  779. def __init__(self, *args, **kwargs):
  780. super(EncryptedPKModel, self).__init__(*args, **kwargs)
  781. setattr(
  782. self.__class__,
  783. &quot;encrypted_%s&quot; % (self._meta.pk.name,),
  784. property(self.__class__._encrypted_pk)
  785. )
  786. def _encrypted_pk(self):
  787. return base36encode(struct.unpack('&lt;Q', self.encryption_obj.encrypt(
  788. str(struct.pack('&lt;Q', self.pk))
  789. ))[0])
  790. encrypted_pk = property(_encrypted_pk)
  791. class Meta:
  792. abstract = True
  793. class ExampleModelManager(EncryptedPKModelManager):
  794. pass
  795. class ExampleModel(EncryptedPKModel):
  796. objects = ExampleModelManager()
  797. example_field = models.CharField(max_length=32)
  798. # Example usage:
  799. # example_instance = ExampleModel.objects.get(pk=1)
  800. # url_pk = example_instance.encrypted_pk
  801. # ExampleModel.objects.get(encrypted_pk=url_pk)
  802. </code></pre></noscript></div>
  803. ]]></content>
  804. </entry>
  805. <entry>
  806. <title type="html"><![CDATA[Replacement for jQuery AlphaNumeric Plugin]]></title>
  807. <link href="http://treyhunner.com/2010/10/replacement-for-jquery-alphanumeric-plugin/"/>
  808. <updated>2010-10-18T00:00:00-07:00</updated>
  809. <id>http://treyhunner.com/2010/10/replacement-for-jquery-alphanumeric-plugin</id>
  810. <content type="html"><![CDATA[<p>I recently inherited a codebase that used the <a href="http://plugins.jquery.com/project/aphanumeric">jQuery AlphaNumeric plugin</a> extensively. This plugin can be used to restrict users from entering certain characters into a form field. The functions included for this plugin (<strong>alphanumeric</strong>, <strong>alpha</strong>, and <strong>numeric</strong>) claim to allow only alphabetic and/or numeric characters to be entered in the form field being acted on.</p>
  811. <p>Unfortunately, this plugin is ineffectual. I have witnessed unexpected behaviors of varying significance associated with this plugin:</p>
  812. <ol>
  813. <li>Forbidden characters can be input by pasting with CTRL+V in Chrome</li>
  814. <li>Forbidden characters can be input by selecting Edit-&gt;Paste in Firefox and IE</li>
  815. <li>Forbidden characters can be input by middle-click pasting in Linux</li>
  816. <li>The arrow keys, Home button, End button, and Delete button do not work in input fields using this plugin&rsquo;s functions in Firefox</li>
  817. <li>The context menu is disabled (right clicking on an input field does nothing)</li>
  818. <li>And most importantly, instead of using a list of allowed characters, a list of disallowed characters is used so these are the only characters that are actually forbidden:
  819. <blockquote><p>!@#$%^&amp;*()+=[]&lsquo;;,/{}|&ldquo;:?~`.&ndash;</p></blockquote></li>
  820. </ol>
  821. <p>The code is so brief that patching the current plugin would be pointless, so instead I wrote <a href="http://github.com/treyhunner/jquery-formrestrict">a replacement</a> that acts similar enough that I did not have to change any of our pre-existing code that depended on the AlphaNumeric plugin.</p>
  822. <p>First I created <strong>restrict</strong>, a very basic modifier function that takes a sanitizer function as an argument. The sanitizer function should manipulate the string to be valid input (if it was not already) and return the valid version of this input. The <strong>restrict</strong> function is triggered whenever the input field is altered and will immediately replace the text in the field with sanitized text.</p>
  823. <p>Most of the restricts I would want to use on an input field can be represented by a regular expression, so I created the <strong>regexRestrict</strong> function that takes a regular expression as input and uses <strong>restrict</strong> to replace matches to this regular expression found in the string.</p>
  824. <p>The <strong>restrict</strong> and <strong>regexRestrict</strong> functions provide every feature that the AlphaNumeric plugin promises, but they don&rsquo;t use the same syntax as the AlphaNumeric plugin. To be able to drop this plugin into a codebase that currently uses the AlphaNumeric plugin, we&rsquo;d need an equivalent to the <strong>alphanumeric</strong>, <strong>alpha</strong>, and <strong>numeric</strong> functions with all of their <a href="http://itgroup.com.ph/alphanumeric/">stated features</a>. To allow the code that relied on the AlphaNumeric plugin to continue functioning, I created replacements for all three of these functions. These functions take the same inputs as their AlphaNumeric plugin equivalents.</p>
  825. <p>The restrict and regexRestrict functions and the alphanumeric plugin replacement that uses these functions can be <a href="http://github.com/treyhunner/jquery-formrestrict">found on github</a>.</p>
  826. ]]></content>
  827. </entry>
  828. <entry>
  829. <title type="html"><![CDATA[Multiple Monitors With Multiple Workspaces]]></title>
  830. <link href="http://treyhunner.com/2009/06/multiple-monitors-with-multiple-workspaces/"/>
  831. <updated>2009-06-15T00:00:00-07:00</updated>
  832. <id>http://treyhunner.com/2009/06/multiple-monitors-with-multiple-workspaces</id>
  833. <content type="html"><![CDATA[<p>In most window managers (WMs) that allow for multiple workspaces, additional monitors simply increase the size of each workspace. Since January I have been using a window manager that handles multiple monitors very differently, <a href="http://www.xmonad.org/">xmonad</a>. Instead of increasing the workspace size to fit onto two monitors, each monitor displays a separate workspace, so the number of visible workspaces is increased.</p>
  834. <p><em>Paradigm difference between these two WM styles:</em></p>
  835. <ol>
  836. <li>Each additional monitor extends the workspace size
  837. <ul>
  838. <li>One large workspace is visible at a time (ex: workspace 1 spans across all monitors)</li>
  839. <li>When the workspace changes, both monitors change</li>
  840. <li>When removing a monitor, workspaces must shrink in size, bunching windows together</li>
  841. </ul>
  842. </li>
  843. <li>Each additional monitor allows another workspace to be visible
  844. <ul>
  845. <li>Each monitor displays one workspace at a time (ex: monitor 1 currently showing workspace 3 and monitor 2 currently showing workspace 1)</li>
  846. <li>When the workspace on one monitor changes the workspace on the other monitor does not need to change</li>
  847. <li>When removing a monitor, one less workspace will be displayed</li>
  848. </ul>
  849. </li>
  850. </ol>
  851. <p>There are many times when I want to be able to keep one monitor static while changing the other monitor. For example I may want a video to stay on one monitor while I work on the other monitor. In most window managers this restricts me to one workspace because changing workspaces would change both monitors. In xmonad, the workspace that contains the video can be placed on one monitor and the visible workspace on the other monitor can changed freely without interfering with the first monitor.</p>
  852. <p>Since using xmonad, I have found &ldquo;1 workspace per monitor&rdquo; window management much more productive and comfortable. I wish more window managers would at least make this kind of workspace/monitor handling an option. I have had problems with xmonad recently and I have been trying to switch back to a more popular window manager with free-floating windows like Gnome, KDE, Xfce, or Openbox.</p>
  853. <p>So far my biggest problem with switching to other window managers has been the lack of &ldquo;1 workspace per monitor&rdquo; support. Xmonad has greatly increased my productivity with two monitors and it&rsquo;s hard for me to switch away from it for this reason. Hopefully I will find out how to effectively emulate this behavior in other window managers.</p>
  854. ]]></content>
  855. </entry>
  856. <entry>
  857. <title type="html"><![CDATA[Ubuntu Now Boots in 10 Seconds]]></title>
  858. <link href="http://treyhunner.com/2009/05/ubuntu-now-boots-in-10-seconds/"/>
  859. <updated>2009-05-01T00:00:00-07:00</updated>
  860. <id>http://treyhunner.com/2009/05/ubuntu-now-boots-in-10-seconds</id>
  861. <content type="html"><![CDATA[<p>I upgraded my Thinkpad to Ubuntu 9.04 (Jaunty Jackalope) recently. My laptop has a solid-state drive and I had tweaked the boot process previously so it was down to 15 seconds. I had heard that Jaunty had drastically decrease the boot time, but I figured my computer could not boot much faster than it already had since I had modified the boot process drastically.</p>
  862. <p>I was wrong. The first time I rebooted I was amazed at the speed of the boot. My boot logger recorded the boot as taking 8 seconds. I have rebooted once more since I installed Jaunty and that boot only took 7 seconds.</p>
  863. <p>The main reason the boot time is so fast is due to my solid state drive. That is how I got my boot down to 15 seconds before. However, 7 seconds is a ridiculously fast boot time in my opinion. Especially since it seems like it takes only about 3 seconds until my login manager is loaded and waiting for me.</p>
  864. ]]></content>
  865. </entry>
  866. </feed>