PageRenderTime 31ms CodeModel.GetById 3ms app.highlight 10ms RepoModel.GetById 1ms 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

Large files files are truncated, but you can click here to view the full file

  1<?xml version="1.0" encoding="utf-8"?>
  2<feed xmlns="http://www.w3.org/2005/Atom">
  3
  4  <title><![CDATA[Trey Hunner]]></title>
  5  <link href="http://treyhunner.com/atom.xml" rel="self"/>
  6  <link href="http://treyhunner.com/"/>
  7  <updated>2014-06-16T18:24:10-07:00</updated>
  8  <id>http://treyhunner.com/</id>
  9  <author>
 10    <name><![CDATA[Trey Hunner]]></name>
 11    
 12  </author>
 13  <generator uri="http://octopress.org/">Octopress</generator>
 14
 15  
 16  <entry>
 17    <title type="html"><![CDATA[CLI for Finding DRM-free Audiobooks]]></title>
 18    <link href="http://treyhunner.com/2014/05/cli-for-finding-drm-free-audiobooks/"/>
 19    <updated>2014-05-14T12:31:00-07:00</updated>
 20    <id>http://treyhunner.com/2014/05/cli-for-finding-drm-free-audiobooks</id>
 21    <content type="html"><![CDATA[<p>I recently acquired an appreciation for audiobooks.  I listen to multiple
 22audiobooks every month and I prefer DRM-free audiobooks so that I can listen
 23through my favorite audiobook reader on each of my devices.</p>
 24
 25<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
 26selection of DRM-free audiobooks.  Unfortunately not all audiobooks are
 27available on either of these sites, so I often end up searching both sites for
 28each book to discover what versions (if any) are available from each.  I got
 29sick of searching both websites all the time, so I just created a Python script
 30to do that work for me.</p>
 31
 32<div><script src='https://gist.github.com/21f307b975027be5162d.js'></script>
 33<noscript><pre><code>#!/usr/bin/env python
 34&quot;&quot;&quot;
 35Search downpour.com and emusic.com for DRM-free audiobooks
 36
 37Usage::
 38
 39    ./find_audiobooks.py &lt;title&gt;...
 40
 41File released to the public domain under CC0 license:
 42http://creativecommons.org/publicdomain/zero/1.0/deed
 43
 44&quot;&quot;&quot;
 45import sys
 46from itertools import chain, izip_longest
 47import urllib2
 48
 49from bs4 import BeautifulSoup
 50from purl import URL
 51
 52
 53def unescape(text):
 54    &quot;&quot;&quot;Return string without smart apostrophes&quot;&quot;&quot;
 55    return text.replace(u'\u2019', &quot;'&quot;)
 56
 57
 58def get_downpour_url(book_name):
 59    &quot;&quot;&quot;Return search URL for downpour.com&quot;&quot;&quot;
 60    base = URL(&quot;http://www.downpour.com/catalogsearch/result/&quot;)
 61    return base.query_param('q', book_name).as_string()
 62
 63
 64def get_emusic_url(book_name):
 65    &quot;&quot;&quot;Return search URL for emusic.com&quot;&quot;&quot;
 66    base_url = URL(&quot;http://www.emusic.com/search/book/&quot;)
 67    return base_url.query_param('s', book_name).as_string()
 68
 69
 70def search_downpour(book_name):
 71    &quot;&quot;&quot;Search Downpour and return list of parsed results&quot;&quot;&quot;
 72    response = urllib2.urlopen(get_downpour_url(book_name))
 73    page = BeautifulSoup(response)
 74    books = page.find_all('li', attrs={'class': &quot;item&quot;})
 75    results = []
 76    for book in books:
 77        header = book.find(attrs={'class': &quot;product-name&quot;})
 78        link_tag = header.find('a')
 79        title = ' '.join(unescape(x)
 80                         for x in link_tag.stripped_strings)
 81        link = link_tag['href']
 82        for author in book.find_all(attrs={'class': 'author'}):
 83            author_text = author.text
 84            if author_text.startswith('By '):
 85                author = author_text[3:]
 86        results.append({
 87            'title': title,
 88            'link': link,
 89            'author': author,
 90        })
 91    return results
 92
 93
 94def search_emusic(book_name):
 95    &quot;&quot;&quot;Search eMusic and return list of parsed results&quot;&quot;&quot;
 96    response = urllib2.urlopen(get_emusic_url(book_name))
 97    page = BeautifulSoup(response)
 98    books = page.find_all('li', attrs={'class': &quot;bundle&quot;})
 99    results = []
100    for book in books:
101        link_tag = book.find('h4').find('a')
102        author_tag = book.find('h5')
103        results.append({
104            'title': link_tag.text,
105            'link': link_tag['href'],
106            'author': author_tag.text,
107        })
108    return results
109
110
111def print_result(result):
112    &quot;&quot;&quot;Print title, author, and link for audiobook result&quot;&quot;&quot;
113    print &quot;Title: {}&quot;.format(result['title'])
114    print &quot;Author: {}&quot;.format(result['author'])
115    print &quot;Link: {}&quot;.format(result['link'])
116    print
117
118
119def merge_lists(*lists):
120    &quot;&quot;&quot;Return merge of lists by alternating elements of each&quot;&quot;&quot;
121    combined_lists = chain.from_iterable(izip_longest(*lists))
122    return list(filter(bool, combined_lists))
123
124
125def main(*args):
126    &quot;&quot;&quot;Search audiobook sites and return search results&quot;&quot;&quot;
127    for book_name in args:
128        results1 = search_emusic(book_name)
129        results2 = search_downpour(book_name)
130        results = merge_lists(results1[:3], results2[:3])
131        for result in results:
132            print_result(result)
133
134
135if __name__ == &quot;__main__&quot;:
136    main(*sys.argv[1:])
137</code></pre></noscript></div>
138
139
140<h3>Future Improvements</h3>
141
142<p>Some ideas for future improvements:</p>
143
144<ul>
145<li>Add <a href="http://www.audible.com/">Audible</a> results when the book cannot be found in a DRM-free format</li>
146<li>Rewrite the script in JavaScript and create a Chrome extension out of it</li>
147<li>Use <a href="https://github.com/kennethreitz/clint">clint</a> to colorize the command-line output</li>
148</ul>
149
150]]></content>
151  </entry>
152  
153  <entry>
154    <title type="html"><![CDATA[Supporting Both Django 1.7 and South]]></title>
155    <link href="http://treyhunner.com/2014/03/migrating-to-django-1-dot-7/"/>
156    <updated>2014-03-27T13:05:00-07:00</updated>
157    <id>http://treyhunner.com/2014/03/migrating-to-django-1-dot-7</id>
158    <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>
159
160<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>
161
162<h2>Assessing your options</h2>
163
164<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>
165
166<ol>
167<li>Move existing <code>migrations</code> directory to <code>south_migrations</code> and create Django 1.7 migrations in <code>migrations</code> package</li>
168<li>Create new Django 1.7 migrations package in <code>django_migrations</code> directory and leave existing South migrations package</li>
169<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>
170</ol>
171
172
173<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>
174
175<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>
176
177<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>
178
179<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>
180<span class='line-number'>2</span>
181<span class='line-number'>3</span>
182</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
183</span><span class='line'><span class="nv">$ </span>python manage.py makemigrations email_log
184</span><span class='line'><span class="nv">$ </span>git add email_log/migrations
185</span></code></pre></td></tr></table></div></figure>
186
187
188<h2>Breaking South support</h2>
189
190<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>
191
192<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>
193
194<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>
195
196<h2>Failing loudly and with a clear error message</h2>
197
198<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>
199
200<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>
201
202<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>
203<span class='line-number'>2</span>
204<span class='line-number'>3</span>
205<span class='line-number'>4</span>
206<span class='line-number'>5</span>
207<span class='line-number'>6</span>
208<span class='line-number'>7</span>
209<span class='line-number'>8</span>
210<span class='line-number'>9</span>
211<span class='line-number'>10</span>
212<span class='line-number'>11</span>
213<span class='line-number'>12</span>
214<span class='line-number'>13</span>
215<span class='line-number'>14</span>
216<span class='line-number'>15</span>
217<span class='line-number'>16</span>
218<span class='line-number'>17</span>
219<span class='line-number'>18</span>
220<span class='line-number'>19</span>
221<span class='line-number'>20</span>
222<span class='line-number'>21</span>
223</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="sd">&quot;&quot;&quot;</span>
224</span><span class='line'><span class="sd">Django migrations for email_log app</span>
225</span><span class='line'>
226</span><span class='line'><span class="sd">This package does not contain South migrations.  South migrations can be found</span>
227</span><span class='line'><span class="sd">in the ``south_migrations`` package.</span>
228</span><span class='line'><span class="sd">&quot;&quot;&quot;</span>
229</span><span class='line'>
230</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>
231</span><span class='line'><span class="s">For South support, customize the SOUTH_MIGRATION_MODULES setting like so:</span>
232</span><span class='line'>
233</span><span class='line'><span class="s">    SOUTH_MIGRATION_MODULES = {</span>
234</span><span class='line'><span class="s">        &#39;email_log&#39;: &#39;email_log.south_migrations&#39;,</span>
235</span><span class='line'><span class="s">    }</span>
236</span><span class='line'><span class="s">&quot;&quot;&quot;</span>
237</span><span class='line'>
238</span><span class='line'><span class="c"># Ensure the user is not using Django 1.6 or below with South</span>
239</span><span class='line'><span class="k">try</span><span class="p">:</span>
240</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>
241</span><span class='line'><span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
242</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>
243</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>
244</span></code></pre></td></tr></table></div></figure>
245
246
247<p>Now when we run <strong>migrate</strong> with Django 1.6 and South, we&rsquo;ll see the following exception raised:</p>
248
249<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>
250<span class='line-number'>2</span>
251<span class='line-number'>3</span>
252<span class='line-number'>4</span>
253<span class='line-number'>5</span>
254<span class='line-number'>6</span>
255<span class='line-number'>7</span>
256</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>
257</span><span class='line'>
258</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>
259</span><span class='line'>
260</span><span class='line'>    <span class="n">SOUTH_MIGRATION_MODULES</span> <span class="o">=</span> <span class="p">{</span>
261</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>
262</span><span class='line'>    <span class="p">}</span>
263</span></code></pre></td></tr></table></div></figure>
264
265
266<h2>Conclusion</h2>
267
268<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>
269
270<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>
271]]></content>
272  </entry>
273  
274  <entry>
275    <title type="html"><![CDATA[TDD With Django Tutorial]]></title>
276    <link href="http://treyhunner.com/2013/11/tdd-with-django-workshop/"/>
277    <updated>2013-11-04T00:00:00-08:00</updated>
278    <id>http://treyhunner.com/2013/11/tdd-with-django-workshop</id>
279    <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>
280
281<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>
282
283<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>
284]]></content>
285  </entry>
286  
287  <entry>
288    <title type="html"><![CDATA[Visual Integration Tests for Django]]></title>
289    <link href="http://treyhunner.com/2013/10/visual-integration-tests-for-django/"/>
290    <updated>2013-10-03T15:19:00-07:00</updated>
291    <id>http://treyhunner.com/2013/10/visual-integration-tests-for-django</id>
292    <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>
293
294<h2>Visual testing frameworks</h2>
295
296<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>
297
298<h3>PhantomCSS</h3>
299
300<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>
301
302<h3>Django-casper</h3>
303
304<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>
305
306<h3>Needle</h3>
307
308<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>
309
310<h2>Django and Needle</h2>
311
312<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>
313
314<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>
315<span class='line-number'>2</span>
316<span class='line-number'>3</span>
317<span class='line-number'>4</span>
318<span class='line-number'>5</span>
319<span class='line-number'>6</span>
320<span class='line-number'>7</span>
321<span class='line-number'>8</span>
322<span class='line-number'>9</span>
323<span class='line-number'>10</span>
324<span class='line-number'>11</span>
325<span class='line-number'>12</span>
326<span class='line-number'>13</span>
327<span class='line-number'>14</span>
328</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>
329</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>
330</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>
331</span><span class='line'>
332</span><span class='line'>
333</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>
334</span><span class='line'>
335</span><span class='line'>    <span class="sd">&quot;&quot;&quot;Needle test case for use with Django live server&quot;&quot;&quot;</span>
336</span><span class='line'>
337</span><span class='line'>    <span class="n">driver</span> <span class="o">=</span> <span class="n">PhantomJS</span>
338</span><span class='line'>
339</span><span class='line'>    <span class="nd">@classmethod</span>
340</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>
341</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>
342</span></code></pre></td></tr></table></div></figure>
343
344
345<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>
346
347<h2>Room for improvement</h2>
348
349<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>
350
351<p>Do you have another solution for visual integration testing?  Let me know in the comments.</p>
352]]></content>
353  </entry>
354  
355  <entry>
356    <title type="html"><![CDATA[Extensible JSON Encoder Using Single-dispatch Functions]]></title>
357    <link href="http://treyhunner.com/2013/09/singledispatch-json-serializer/"/>
358    <updated>2013-09-27T00:00:00-07:00</updated>
359    <id>http://treyhunner.com/2013/09/singledispatch-json-serializer</id>
360    <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>
361
362<h2>JSON Encoding using the json library</h2>
363
364<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>
365
366<ul>
367<li>Allowing specification of custom encoder class by client applications</li>
368<li>Overriding the default JSON encoder class (or a client-specified one) for any further extensions</li>
369<li>Passing JSON encoder classes into other serialization libraries used by your application</li>
370</ul>
371
372
373<h3>Combining JSON encoders</h3>
374
375<p>If you need to compose two custom JSON encoders specified in two different packages, you may need to:</p>
376
377<ul>
378<li>Use multiple inheritance and hope the encoders play nicely together</li>
379<li>Duplicate code from one of the packages and create a new serializer with single inheritance</li>
380<li>Monkey patch one or both of the libraries</li>
381</ul>
382
383
384<h2>JSON encoder using single-dispatch generic functions</h2>
385
386<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>
387
388<div><script src='https://gist.github.com/6734816.js?file=example.py'></script>
389<noscript><pre><code>from decimal import Decimal
390
391from json_singledispatch import encode
392
393
394@encode.register(set)
395def encode_set(obj):
396    return encode(list(obj))
397
398
399@encode.register(Decimal)
400def encode_dict(obj):
401    return encode(str(obj))
402
403
404print encode({'key': &quot;value&quot;})
405print encode({5, 6})
406print encode(Decimal(&quot;5.6&quot;))</code></pre></noscript></div>
407
408
409<p>As you can see, it&rsquo;s fairly easy to extend the encoder to understand serialization rules for new data types.</p>
410
411<p>The impementation is fairly simple, albeit a hack:</p>
412
413<div><script src='https://gist.github.com/6734816.js?file=json_singledispatch.py'></script>
414<noscript><pre><code>import json
415from singledispatch import singledispatch
416
417
418class _CustomEncoder(json.JSONEncoder):
419    def default(self, obj):
420        for obj_type, handler in encode.registry.items():
421            if isinstance(obj, obj_type) and obj_type is not object:
422                return handler(obj)
423        return super(_CustomEncoder, self).default(obj)
424
425
426@singledispatch
427def encode(obj, **kwargs):
428    return json.dumps(obj, cls=_CustomEncoder, **kwargs)</code></pre></noscript></div>
429
430
431<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>
432
433<h2>Related Links</h2>
434
435<ul>
436<li><a href="http://lukasz.langa.pl/8/single-dispatch-generic-functions/">What single-dispatch generic functios mean for you</a></li>
437<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>
438</ul>
439
440]]></content>
441  </entry>
442  
443  <entry>
444    <title type="html"><![CDATA[Test-Inspired Development]]></title>
445    <link href="http://treyhunner.com/2013/07/test-inspired-development/"/>
446    <updated>2013-07-28T00:00:00-07:00</updated>
447    <id>http://treyhunner.com/2013/07/test-inspired-development</id>
448    <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>
449
450<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>
451
452<ol>
453<li>Write a bit of code</li>
454<li>Write a bit of a unit test for your code</li>
455<li>Ensure the test succeeds, but fails when the code is commented out or git stash-ed</li>
456<li>Repeat steps 1, 2, and 3 as needed</li>
457<li>Start writing an integration test, fleshing out as far as you can</li>
458<li>Make sure the integration test fails at the correct point</li>
459<li>Repeat steps 5 and 6 as needed</li>
460<li>Make sure no commit you make along the way contains both tests and code</li>
461<li>Use git rebase if necessary to ensure the test commits come before the code commits</li>
462</ol>
463
464
465<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>
466
467<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>
468
469<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>
470
471<p>Have questions about my testing methodology?  Do you use a different technique?  Feel free to share your thoughts in the comments below.</p>
472]]></content>
473  </entry>
474  
475  <entry>
476    <title type="html"><![CDATA[Maintaining an Open Source Project]]></title>
477    <link href="http://treyhunner.com/2013/07/maintaining-an-open-source-project/"/>
478    <updated>2013-07-21T00:00:00-07:00</updated>
479    <id>http://treyhunner.com/2013/07/maintaining-an-open-source-project</id>
480    <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>
481
482<h2>Structure your project logically</h2>
483
484<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>
485
486<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>
487
488<h2>Make it very easy to contribute</h2>
489
490<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>
491
492<h2>Be kind</h2>
493
494<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>
495
496<h2>Be Responsive</h2>
497
498<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>
499
500<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>
501
502<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>
503
504<h2>Automate all the things</h2>
505
506<p>Use:</p>
507
508<ul>
509<li><a href="https://travis-ci.org/">Travis CI</a>: run continuous integration tests for all pushes and pull requests</li>
510<li><a href="https://coveralls.io/">Coveralls</a>: measure code coverage while running your CI tests</li>
511<li>A documentation site like <a href="https://readthedocs.org/">Read The Docs</a> which auto-updates on pushes</li>
512</ul>
513
514
515<h2>Learn from others</h2>
516
517<p>I&rsquo;m not the only one with opinions about open source.</p>
518
519<p>Read more opinions on maintaining an open source project:</p>
520
521<ul>
522<li><a href="https://segment.io/blog/tips-for-maintaining-an-open-source-library/">Tips for maintaining an Open-Source library</a></li>
523<li><a href="http://www.codesimplicity.com/post/open-source-community-simplified/">Open Source Community, Simplified</a></li>
524<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>
525</ul>
526
527
528<p>Watch some videos about maintaining an open source project:</p>
529
530<ul>
531<li><a href="http://www.youtube.com/watch?v=xgWFTrXn0_U">Maintaining Your Sanity While Maintaining Your Open Source App</a></li>
532<li><a href="http://pyvideo.org/video/1795/write-the-docs">Write the Docs</a></li>
533</ul>
534
535]]></content>
536  </entry>
537  
538  <entry>
539    <title type="html"><![CDATA[Log All Outgoing Emails in Django]]></title>
540    <link href="http://treyhunner.com/2013/05/django-email-log/"/>
541    <updated>2013-05-20T00:00:00-07:00</updated>
542    <id>http://treyhunner.com/2013/05/django-email-log</id>
543    <content type="html"><![CDATA[<p>Ever needed to determine whether an email was sent from a Django project?  I
544made a Django application that does exactly that: <a href="https://github.com/treyhunner/django-email-log">django-email-log</a>.</p>
545
546<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
547application out of it.  All emails are stored in a single model which can
548easily be viewed, searched, sorted, and filtered from the admin site.</p>
549
550<p>I used test-driven development when making the app and I baked in Python 3
551support from the beginning.  I found the process of TDD for a standalone
552Python package fairly easy and enjoyable.</p>
553]]></content>
554  </entry>
555  
556  <entry>
557    <title type="html"><![CDATA[Django-simple-history Is Back]]></title>
558    <link href="http://treyhunner.com/2013/05/django-simple-history/"/>
559    <updated>2013-05-01T00:00:00-07:00</updated>
560    <id>http://treyhunner.com/2013/05/django-simple-history</id>
561    <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>
562
563<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>
564
565<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>
566]]></content>
567  </entry>
568  
569  <entry>
570    <title type="html"><![CDATA[Random Name Generator Website]]></title>
571    <link href="http://treyhunner.com/2013/03/pseudorandom.name/"/>
572    <updated>2013-03-10T00:00:00-08:00</updated>
573    <id>http://treyhunner.com/2013/03/pseudorandom.name</id>
574    <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
575random names.  I&rsquo;ve now used this Python library to make a basic Flask website
576that generates random names.  You can visit it at:
577<a href="http://www.pseudorandom.name">http://www.pseudorandom.name</a></p>
578
579<h2>It&rsquo;s responsive</h2>
580
581<p>The font size and margin on pseudorandom.name change based on the web browser
582width and height.  I determined the font sizes I wanted to use for each screen
583width by using the longest first and last name in the name files I&rsquo;m using (11
584and 13 characters respectively).  The website looks reasonable on various
585desktop resolutions and on phone screens.</p>
586
587<h2>It&rsquo;s an HTTP API</h2>
588
589<p>If curl is used to access the site, plain text is returned instead of an HTML
590webpage.  For example:</p>
591
592<pre><code>curl www.pseudorandom.name
593</code></pre>
594
595<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>.
596Currently <a href="http://pseudorandom.name">http://pseudorandom.name</a> redirects to <a href="http://www.pseudorandom.name">http://www.pseudorandom.name</a> so
597using the bare domain requires telling curl to follow redirects with <code>-L</code>:</p>
598
599<pre><code>curl -L pseudorandom.name
600</code></pre>
601
602<h2>More to come?</h2>
603
604<p>It might be nice to allow generating multiple names at once, generating
605gender-specific names, and maybe providing other content types (JSON).  The
606HTML version could also use a cleaner, more feature-full design.</p>
607
608<p>Also it would probably be more efficient to use a SQL database for querying
609random names so I may eventually abandon the names library and use a database
610for querying.</p>
611
612<p>The website&rsquo;s source code is <a href="https://github.com/treyhunner/pseudorandom.name">hosted on Github</a> and provided under an
613<a href="http://th.mit-license.org/2013">MIT license</a>.  Feel free to fork it or submit an issue.</p>
614]]></content>
615  </entry>
616  
617  <entry>
618    <title type="html"><![CDATA[Random Name Generator in Python]]></title>
619    <link href="http://treyhunner.com/2013/02/random-name-generator/"/>
620    <updated>2013-02-17T00:00:00-08:00</updated>
621    <id>http://treyhunner.com/2013/02/random-name-generator</id>
622    <content type="html"><![CDATA[<p>I&rsquo;ve used multiple websites to generate random names for my test data when
623running manual or automated QA tests.</p>
624
625<p>Since discovering DuckDuckGo&rsquo;s <a href="https://duckduckgo.com/?q=random+word">random word</a> generation, I&rsquo;ve
626<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
627random names also.</p>
628
629<p>I didn&rsquo;t want to write my own DuckDuckGo plugin yet so I made a Python-powered
630command line tool instead using <a href="https://www.census.gov/genealogy/www/data/1990surnames/index.html">1990 Census data</a>.</p>
631
632<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>
633
634<h3>It&rsquo;s really simple</h3>
635
636<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>.
637There is only one feature available from the command line currently: generate a
638single random full name.  There&rsquo;s a few more features if importing as a Python
639package: generate random last name or generate random first name (with or
640without specifying gender), generate random full name (also without or without
641gender).</p>
642
643<p>The random name picker relies on the cumulative frequencies listed in the
644included Census data files.  Here&rsquo;s the steps that are taken:
6451. A random floating point number is chosen between 0.0 and 90.0
6462. Name file lines are iterated through until a cumulative frequency is found
647that is less than the randomly generated number
6483. The name on that line is chosen and returned (or printed out)</p>
649
650<h3>Examples</h3>
651
652<p>Here&rsquo;s how you use it from the command line:</p>
653
654<pre><code>$ names
655Kara Lopes
656</code></pre>
657
658<p>Here&rsquo;s how you use it as a Python package:</p>
659
660<pre><code>&gt;&gt;&gt; import names
661&gt;&gt;&gt; names.get_full_name()
662u'Patricia Halford'
663&gt;&gt;&gt; names.get_full_name(gender='male')
664u'Patrick Keating'
665&gt;&gt;&gt; names.get_first_name()
666'Bernard'
667&gt;&gt;&gt; names.get_first_name(gender='female')
668'Christina'
669&gt;&gt;&gt; names.get_last_name()
670'Szczepanek'
671</code></pre>
672]]></content>
673  </entry>
674  
675  <entry>
676    <title type="html"><![CDATA[Tmuxstart]]></title>
677    <link href="http://treyhunner.com/2012/12/tmuxstart/"/>
678    <updated>2012-12-15T00:00:00-08:00</updated>
679    <id>http://treyhunner.com/2012/12/tmuxstart</id>
680    <content type="html"><![CDATA[<p>I&rsquo;ve been using a helper script to manage all of my tmux sessions for the last
681few months (nearly since the time I switched from screen to tmux).</p>
682
683<h3>A tmuxinator alternative</h3>
684
685<p>I use a separate session for each project I work on and I wanted a way to
686easily connect to project sessions with the same default windows and
687environment variables each time.  I tried using <a href="https://github.com/aziz/tmuxinator">tmuxinator</a>, but I found it
688to be too complicated for my needs.  I had trouble working within the
689limitations of the yaml files and I found the installation to be overly
690complicated (especially when working on a new server that didn&rsquo;t have ruby
691installed).  After I realized how simple it would be to solve my own problem I
692made the tmuxstart script.</p>
693
694<h3>Now available on Github</h3>
695
696<p>While updating my <a href="https://github.com/treyhunner/dotfiles">dotfiles</a> repository I realized I hadn&rsquo;t yet incorporated
697my tmuxstart workflow into my <code>.tmux.conf</code> file.  Yesterday I decided to make
698README and LICENSE files for my script and put it under version control.  I
699also added some helper functions to make the script even easier to use.  It&rsquo;s
700now available in my <a href="https://github.com/treyhunner/tmuxstart">tmuxstart</a> repository on Github.</p>
701
702<h3>Example usage</h3>
703
704<p>Below is an example of how to use tmuxstart.</p>
705
706<ol>
707<li>Add the following line to your <code>~/.tmux.conf</code> file:</li>
708</ol>
709
710
711<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
712</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>
713
714
715<ol>
716<li>Create a <code>~/.tmuxstart</code> directory:</li>
717</ol>
718
719
720<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
721</pre></td><td class='code'><pre><code class=''><span class='line'>mkdir ~/.tmuxstart</span></code></pre></td></tr></table></div></figure>
722
723
724<ol>
725<li>Create a file <code>~/.tmuxstart/main</code> with the following contents:</li>
726</ol>
727
728
729<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
730<span class='line-number'>2</span>
731</pre></td><td class='code'><pre><code class=''><span class='line'>new_session htop
732</span><span class='line'>new_window</span></code></pre></td></tr></table></div></figure>
733
734
735<p>Now you can create a tmux session called &ldquo;main&rdquo; with <a href="http://htop.sourceforge.net/">htop</a> in the first
736window and a shell in the second window by executing:</p>
737
738<pre><code>tmuxstart main
739</code></pre>
740
741<p>Or from an existing tmux server type <code>&lt;PREFIX&gt; S</code> (<code>&lt;PREFIX&gt;</code> is Ctrl-B by
742default), type <code>main</code> and hit Enter.</p>
743
744<p>When using either of these methods if a session called &ldquo;main&rdquo; already exists
745the existing session will be used instead.</p>
746
747<h3>How I use this</h3>
748
749<p>I have a separate tmuxstart session file for each of my Django projects and
750some of my open source projects also.  I use a &ldquo;main&rdquo; session file similar to
751the one above and I have Gnome Terminal set to start with the custom command
752&ldquo;tmuxstart main&rdquo; instead of a shell.</p>
753]]></content>
754  </entry>
755  
756  <entry>
757    <title type="html"><![CDATA[Maintaining Consistent Coding Conventions With EditorConfig]]></title>
758    <link href="http://treyhunner.com/2012/02/editorconfig/"/>
759    <updated>2012-02-17T00:00:00-08:00</updated>
760    <id>http://treyhunner.com/2012/02/editorconfig</id>
761    <content type="html"><![CDATA[<p>I often have indentation issues when coding because I work with many open
762source (and closed source) projects that have different identation styles.  For
763example, my <a href="https://github.com/treyhunner/jquery-formrestrict">jQuery formrestrict</a> uses 4 spaces for indentation,
764<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.
765I don&rsquo;t want to change my text editor&rsquo;s indentation settings every time I open
766one of these files and using a plugin to auto-detect indentation is only a
767partial solution.  To solve this problem I started a project I call
768EditorConfig.</p>
769
770<p><a href="http://editorconfig.org">EditorConfig</a> defines coding conventions for groups of files and instructs
771text editors to adhere to these conventions.  To solve my problem, I just
772create a file called <code>.editorconfig</code> that sets indentation for my files and
773with my EditorConfig plugin installed Vim takes care of the rest.  Here&rsquo;s an
774example <code>.editorconfig</code> file I could add to my project that uses
775<a href="https://github.com/kswedberg/jquery-expander">jQuery expander</a> and <a href="https://github.com/gbirke/jquery_pagination">jQuery pagination</a>:</p>
776
777<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>
778<span class='line-number'>2</span>
779<span class='line-number'>3</span>
780<span class='line-number'>4</span>
781<span class='line-number'>5</span>
782<span class='line-number'>6</span>
783<span class='line-number'>7</span>
784<span class='line-number'>8</span>
785<span class='line-number'>9</span>
786<span class='line-number'>10</span>
787<span class='line-number'>11</span>
788</pre></td><td class='code'><pre><code class='ini'><span class='line'><span class="k">[*.js]</span>
789</span><span class='line'><span class="na">indent_style</span> <span class="o">=</span> <span class="s">space</span>
790</span><span class='line'><span class="na">indent_size</span> <span class="o">=</span> <span class="s">4</span>
791</span><span class='line'>
792</span><span class='line'><span class="k">[jquery.expander.js]</span>
793</span><span class='line'><span class="na">indent_style</span> <span class="o">=</span> <span class="s">space</span>
794</span><span class='line'><span class="na">indent_size</span> <span class="o">=</span> <span class="s">2</span>
795</span><span class='line'>
796</span><span class='line'><span class="k">[jquery.pagination.js]</span>
797</span><span class='line'><span class="na">indent_style</span> <span class="o">=</span> <span class="s">tab</span>
798</span><span class='line'><span class="na">indent_size</span> <span class="o">=</span> <span class="s">4</span>
799</span></code></pre></td></tr></table></div></figure>
800
801
802<p>With this <code>.editorconfig</code> file, all JavaScript files use 4 spaces for
803indentation except for <code>jquery.expander.js</code> which uses 2 spaces and
804<code>jquery.pagination.js</code> which uses hard tabs with a column width of 4.  If I put
805my <code>.editorconfig</code> file under version control, other developers working on my
806project can see the coding conventions I defined and if their text editor has
807an EditorConfig plugin installed their editor will automatically use the
808correct indentation as well.</p>
809
810<p>Example EditorConfig files can be seen in my own projects (such as
811<a href="https://github.com/treyhunner/dotfiles/blob/master/.editorconfig">in my dotfiles repo</a>) and in the
812<a href="https://github.com/treyhunner/dotfiles/blob/master/.editorconfig">various EditorConfig plugin codebases</a>.  More
813information on EditorConfig can be found at the
814<a href="http://editorconfig.org">EditorConfig website</a>.</p>
815
816<p>EditorConfig plugins are <a href="http://editorconfig.org/#download">available for 6 different editors</a>
817now.  If you like the idea, <a href="http://editorconfig.org">try out EditorConfig</a> for your
818favorite editor and <a href="https://groups.google.com/forum/?fromgroups#!forum/editorconfig">tell us what you think</a>.  If you don&rsquo;t like
819the idea or have a problem with EditorConfig please <a href="https://github.com/editorconfig/editorconfig/issues">submit an issue</a>,
820add a thread to <a href="https://groups.google.com/forum/?fromgroups#!forum/editorconfig">the mailing list</a>, or send me an email voicing
821your concern.</p>
822]]></content>
823  </entry>
824  
825  <entry>
826    <title type="html"><![CDATA[Migrating From Subversion to Git]]></title>
827    <link href="http://treyhunner.com/2011/11/migrating-from-subversion-to-git/"/>
828    <updated>2011-11-17T00:00:00-08:00</updated>
829    <id>http://treyhunner.com/2011/11/migrating-from-subversion-to-git</id>
830    <content type="html"><![CDATA[<p>I recently migrated multiple Subversion repositories to Git.  I found
831<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
832some tips I found helpful that were not mentioned in that post.</p>
833
834<h3>Generating an authors file</h3>
835
836<p>Subversion denotes authors by usernames while Git uses name and email.  An
837authors file can be used to map subversion usernames to Git authors when
838creating the Git repository, like so:</p>
839
840<pre><code>git-svn clone --stdlayout --authors-file=authors.txt http://your/svn/repo/url
841</code></pre>
842
843<p>The trickest part about <a href="http://triptico.com/n…

Large files files are truncated, but you can click here to view the full file