はてなブログのヘッダーにGitHubのコントリビューションを表示

はじめに

ヘッダに草生やしました。

f:id:hiroki-sawano:20200711051612p:plain

生やし方

github-calendar を使います。

github.com

Design > Customize > Headerに移動し、下図赤枠内の Below the titleREADMEに記載のコードを貼り付けるだけです。

f:id:hiroki-sawano:20200711053213p:plain

スマホで表示が崩れる場合

スマホiPhone 11 ProのChrome)で見たら崩れてました。

f:id:hiroki-sawano:20200721204517p:plain
変更前

.calendarwidth を指定すると整いました。

<div class="calendar" style="width: 95%">
    Loading the data just for you.
</div>

f:id:hiroki-sawano:20200721204538p:plain
変更後

Django 2でモデルにMeta.orderingを指定すると.values().annotate()が不正な結果を返す問題

はじめに

Django(2.1.7)で集計処理( values().annotate() )が Meta.ordering を追加した途端に誤った結果を返すようになりました。

問題

例として以下の Foo モデルを用意します。 ここで created_at フィールドを Meta.ordering に指定しておきます。

class Foo(models.Model):
    foo = models.CharField(max_length=255) #  GROUP BYに指定するフィールド
    bar = models.IntegerField() #  SUM()に指定するフィールド
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['created_at', ]

まず Foo モデルに集計対象となるデータを追加します。

❯ python manage.py shell
>>> from example.models import Foo
>>> Foo.objects.create(foo="1", bar=10)
>>> Foo.objects.create(foo="1", bar=10)
>>> Foo.objects.create(foo="2", bar=10)

続けて、 foo フィールドをGROUP BY項目として bar フィールドを集計します。
しかし、結果として返される QuerySet は集計されていません。

>>> from django.db.models import Sum
>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar'))
<QuerySet [{'foo': '1', 'sum': 10}, {'foo': '1', 'sum': 10}, {'foo': '2', 'sum': 10}]>

原因

この事象については公式ドキュメントに記載があります。

docs.djangoproject.com

Deprecated since version 2.2: Starting in Django 3.1, the ordering from a model’s Meta.ordering won’t be used in GROUP BY queries, such as .annotate().values(). Since Django 2.2, these queries issue a deprecation warning indicating to add an explicit order_by() to the queryset to silence the warning.

Django 2では Meta.orderingvalues().annotate() の結果に影響するため明示的に order_by() を指定する必要があります。 なお、Django 2.2からは警告が出力され、Django 3.1(2020/8リリース予定)では Meta.ordering が無視されるように修正されるとのことです。

対策

order_by() を追加し、期待通り集計できることを確認します。

>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar')).order_by('foo')
<QuerySet [{'foo': '1', 'sum': 20}, {'foo': '2', 'sum': 10}]>
>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar')).order_by('bar')
<QuerySet [{'foo': '1', 'sum': 20}, {'foo': '2', 'sum': 10}]>
>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar')).order_by('created_at') #  クエリに含まないフィールドの指定では不正な結果を返す
<QuerySet [{'foo': '1', 'sum': 10}, {'foo': '1', 'sum': 10}, {'foo': '2', 'sum': 10}]>

なお、 Meta.ordering で例えば foo フィールドを指定していた場合には問題なく集計できます。 values().annotate() で構成するクエリに含まないフィールドで order_by が適用されてしまうことが問題のようです。

まとめ

Django 2を使用していて集計処理がうまくいかない場合は Meta.ordering を疑ってみると良いかもしれません。

Neovim v0.4.3でファイルを開くと一文字目がgに置換され謎のコマンドが入力される話

はじめに

先日macOSをMojave 10.14.6からCatalina 10.15.5にアップデートしたところ、Neovimでファイルを開いた際に一文字目が g に置換され、コマンドラインに謎の文字列 0d3d/0df6/1130^G が入力されるようになりました。

f:id:hiroki-sawano:20200709220038p:plain

原因

Neovimの不具合にぶつかったみたいです。対応するIssueを見つけました。

github.com

暫定対策

本IssueをクローズするPRはv0.5で取り込まれそうですが、残念ながらまだ修正されていないようです..。
このコメントにあるように暫定的に cnoremap1130 が入力されると変更を取り消すようにします( 1130 の部分はターミナルの背景色で変わるらしい)。

cnoremap 1130 <C-u>undo<CR>

しかしこれだとファイル編集中にエディタを Ctrl-z で一時停止し、 fg で再開した際に直近の変更が消えたりします..。
より良いワークアラウンドを見つけたら更新します。v0.5のリリースが非常に待ち遠しい..。

プライベートリポジトリにCodecovを導入

はじめに

Codecovの導入手順をまとめます。 オープンソースなプロジェクトであれば無料ですが、プライベートリポジトリでも5ユーザまでであればFreeプランで無制限に使用できます。
以前はFreeプランで使用できるプライベートリポジトリは1つだけだったようですが、ありがたいことに最近料金プランが変わったみたいです。

blog.codecov.io

What is changing? Codecov is moving from per repository pricing to per user pricing: https://codecov.io/pricing.

For organizations with private repositories, Codecov is now free for the first 5 users, and all plans now include unlimited private repos.

Codecovでアカウント登録

サインアップページSign up with GitHub からアカウントを作成します。手順通り進めるだけなので省略します。

リポジトリの追加とトークンの取得

Add a repositoryを選択

f:id:hiroki-sawano:20200708212516p:plain

Codecovを導入するリポジトリを選択

f:id:hiroki-sawano:20200708212601p:plain

表示されるトークンをコピー

f:id:hiroki-sawano:20200708212825p:plain

GitHubリポジトリトークンを登録

Settings > Secrets > New secret から CODECOV_TOKEN という名前でコピーしたトークンを登録します。

f:id:hiroki-sawano:20200708213244p:plain

登録後は以下の通りSecretsが表示されます。

f:id:hiroki-sawano:20200708211242p:plain

GitHub Actionsのワークフローからカバレッジレポートをアップロード

Codecov GitHub Action を使用し、ワークフロー内で作成したカバレッジレポートをCodecovにアップロードします。

github.com

ワークフロー のYAMLファイルでビルド後(カバレッジレポート出力後)に以下のステップを追加するだけです。
Codecov GitHub Action は自動的にレポートファイルを検出してくれます。
全てのPRで実行したいのでトリガーに on.pull_request を設定しておきます。

name: build
on:
  push:
    branches: [ master ]
  pull_request:

jobs:
  build:
    runs-on: ...
    steps:
    # ~ビルドのステップは省略~
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v1
      with:
        token: ${{ secrets.CODECOV_TOKEN }}

これでPRを契機に以下の通りカバレッジレポートがCodecovにアップロードされます(JaCoCoのレポートで試してます)。

f:id:hiroki-sawano:20200708215206p:plain

アップロードが正常に終了すると、ボットがマージ先ブランチのカバレッジの変化をPRにコメントしてくれます。

f:id:hiroki-sawano:20200708215408p:plain

バッジをREADMEに追加

Codecovでリポジトリのページから Settings > Badge にアクセスするとリンクを取得できます。

f:id:hiroki-sawano:20200708220346p:plain

このリンクをREADMEのファイルに貼り付けることで以下のようにカバレッジ率を表示することができます。

f:id:hiroki-sawano:20200708220757p:plain

CentOS 7にJupyterHub 1.0を導入(SSL対応/NativeAuthenticator/DockerSpawner)

はじめに

Jupyter Notebookにユーザ管理機能を追加したJupyterHub(v1.0.0)を構築する方法をまとめます。

github.com

JupyterHubはデフォルトだとPAM認証なのでLinuxのユーザを作成(adduser/passwd)しなければなりませんし、ユーザのサインアップやパスワードの変更機能が提供されていないので実運用するには色々と難点があるんですが、あまり良い環境構築手順がなく苦労しました...。

以降ではこんなことをやります。最低限の構成としてはこれぐらいあれば良いんじゃないかと思います。

  • Apacheでリバースプロキシ
  • SSL対応
  • NativeAuthenticatorでユーザのサインアップ・承認、パスワード変更機能を追加
  • DockerSpawnerでNotebookサーバをDockerコンテナで生成

前提

rootに変更

$ sudo su -

Miniconda3のインストール

Anacondaでも良いです。

# wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
# bash Miniconda3-latest-Linux-x86_64.sh

...
Do you accept the license terms? [yes|no]
[no] >>> yes

Miniconda3 will now be installed into this location:
/root/miniconda3

  - Press ENTER to confirm the location
  - Press CTRL-C to abort the installation
  - Or specify a different location below

[/root/miniconda3] >>> /opt/miniconda3
...
Do you wish the installer to initialize Miniconda3
by running conda init? [yes|no]
[no] >>> yes

# source ~/.bashrc
# rm Miniconda3-latest-Linux-x86_64.sh

Conda環境の作成

Pythonは3.7にしておきます。 anacondaに関しては、標準のSpawnerである LocalProcessSpawner を使う場合に色々な便利ライブラリが欲しかったので指定しましたが、 結局DockerSpawnerを使うのでいらないと思います。

# conda create -n py37 python=3.7 anaconda
# conda activate py37

JupyterHubのインストール

本記事執筆時点で最新バージョンは1.0.0です。

# conda install -c conda-forge jupyterhub

NativeAuthenticatorのインストール

ユーザのサインアップ・承認、パスワード変更機能を追加するため、NativeAuthenticatorを導入します。 最新版が欲しいので直接リポジトリからインストールします。

# yum install git
# cd ~
# pip install -e git+https://github.com/jupyterhub/nativeauthenticator.git@919a37460cdb46ef536985c0cb0c1109d5e0e483#egg=jupyterhub_nativeauthenticator
# cd src/jupyterhub-nativeauthenticator/nativeauthenticator/

ただどうやら上記のバージョンだとJupyterHubのv1.0.0に対応してないらしく、パスワード変更でエラーになってしまいました。 本家にIssue作成しときましたが、とりあえずコードを直修正します。

github.com

handler.ChangePasswordHandler.post のユーザ取得処理に await を追加すれば問題は解消しました。

124     @web.authenticated
125     async def post(self):
126         #user = self.get_current_user()
127         user = await self.get_current_user()

DockerSpawnerのインストール

デフォルトのLocalProcessSpawnerだとLinuxユーザが存在しなければならず煩わしいので、 ユーザごとのNotebookサーバをDockerコンテナとして立ち上げるためDockerSpawnerを導入します。

# pip install dockerspawner

Dockerのインストール

Dockerがないのであればインストールします。

# yum install -y yum-utils device-mapper-persistent-data lvm2
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# yum install -y docker-ce docker-ce-cli containerd.io
# systemctl start docker
# systemctl enable docker

Hub APIのポートを解放

Dockerコンテナからホストで動作するJupyterHubのAPIにアクセスするためFWの設定を変更します。

# firewall-cmd --add-port=8081/tcp --zone=public --permanent
# firewall-cmd --reload

設定ファイルの作成

# mkdir /etc/jupyterhub
# cd /etc/jupyterhub
# jupyterhub --generate-config
# JupyterHub自体はlocalhostで起動
c.JupyterHub.ip = '127.0.0.1'

# Adminユーザを任意の名前で指定
c.Authenticator.admin_users = {'jupyter'}

# DBはSQLite3
c.JupyterHub.db_url = 'sqlite:////etc/jupyterhub/jupyterhub.sqlite'

# NativeAuthenticatorを指定
c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'

# DockerSpawner関係の設定
c.JupyterHub.hub_connect_ip = '<jupyterhub-server-ip>'
c.JupyterHub.hub_ip = '0.0.0.0'
c.JupyterHub.hub_port = 8081
c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
c.DockerSpawner.hub_connect_ip = '10.0.1.4'

## pandas、matplotlib、seaborn、scikit-learnなどが含まれたscipy-notebookを採用
## https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html#jupyter-scipy-notebook
c.DockerSpawner.image = 'jupyter/scipy-notebook:6c3390a9292e'

## ノートブックの永続化設定
notebook_dir = '/home/jovyan/work'
c.DockerSpawner.notebook_dir = notebook_dir
c.DockerSpawner.volumes = { 'jupyterhub-user-{username}': notebook_dir }

JupyterHubサービスの設定

[Unit]
Description=Jupyterhub

[Service]
User=root
Environment="PATH=/opt/miniconda3/envs/py37/bin:/opt/miniconda3/condabin:/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin"
ExecStart=/opt/miniconda3/envs/py37/bin/jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
WorkingDirectory=/etc/jupyterhub

[Install]
WantedBy=multi-user.target

JupyterHubを再起動します。

# systemctl enable jupyterhub
# systemctl start jupyterhub

Apacheの設定

Using a reverse proxyを参考に設定します。

<VirtualHost *:443>
  ServerAdmin <server-admin>
  ServerName <jupyterhub-server-hostname>

  # Configure SSL
  SSLEngine on
  SSLProtocol all -SSLv2 -SSLv3
  SSLCipherSuite HIGH:3DES:!aNULL:!MD5:!SEED:!IDEA
  SSLCertificateFile <ssl-certificate-file>
  SSLCertificateKeyFile <ssl-certificate-key-file>
  SSLCertificateChainFile <ssl-certificate-chain-file>

  # Use RewriteEngine to handle websocket connection upgrades
  RewriteEngine On
  RewriteCond %{HTTP:Connection} Upgrade [NC]
  RewriteCond %{HTTP:Upgrade} websocket [NC]
  RewriteRule /(.*) ws://127.0.0.1:8000/$1 [P,L]

  <Location "/">
    # Preserve Host header to avoid cross-origin problems
    ProxyPreserveHost on
    # Proxy to JupyterHub
    ProxyPass         http://127.0.0.1:8000/
    ProxyPassReverse  http://127.0.0.1:8000/
  </Location>
</VirtualHost>

SELinuxの無効化

SELINUX=disabled

再起動

# shutdown -r now

JupyterHubにアクセス

https://jupyterhub-server-hostname にアクセスし、ログイン画面が表示されることを確認します。

Adminユーザの作成

Adminユーザを作成するため、Signup!リンクからサインアップ画面へ遷移します。

image

ユーザ名にはjupyterhub_config.pyc.Authenticator.admin_usersに指定したjupyterを入力します。

image

Your information have been sent to the admin と表示されますが、Adminとして設定したjupyterについては自身がAdminなのですぐさまログインできるようになります。

image

Adminユーザでログイン

Login!リンクからログイン画面へ遷移し、上記で入力したアカウント情報を用いてログインします。

image

一般ユーザの作成

上記の手順と同様にサインアップ画面から任意のユーザ名、パスワードでユーザを作成します。
ただし、この時点で一般ユーザはログインすることができず、Adminユーザによる承認が必要となります。

image

image

image

Adminユーザでログインし、/hub/authorizeにアクセスし、新規作成されたユーザを承認することでアカウントが有効になります。

image

image

image

image

パスワードの変更

パスワードの変更は/hub/change-passwordから実施できます。

image

ユーザの管理

AdminユーザはControl Panel>Adminからユーザの管理(Notebookサーバの停止、ユーザの削除など)を行うことができます。 ただし、Add Usersでユーザを作成してもパスワードが設定されないのでログインができません。使用しないことをおすすめします。

image

Jupyterノートブックの保存先

各ユーザのJupyterサーバはDockerコンテナとして立ち上がります。

# docker ps
CONTAINER ID        IMAGE                                 COMMAND                  CREATED             STATUS              PORTS                       NAMES
350c3f757230        jupyter/scipy-notebook:6c3390a9292e   "tini -g -- start-no…"   About an hour ago   Up About an hour    127.0.0.1:32911->8888/tcp   jupyter-test
bb36d90c7890        jupyter/scipy-notebook:6c3390a9292e   "tini -g -- start-no…"   2 hours ago         Up 2 hours          127.0.0.1:32910->8888/tcp   jupyter-jupyter

Jupyterノートブックは各コンテナ上の/home/jovyan/work配下に保存されます。

# docker exec -it jupyter-jupyter bash
$ cd work/
$ pwd
/home/jovyan/work
$ ls
foo.ipynb

DataTables(+Django)でAjax通信

はじめに

DataTables 1.10.12 (https://datatables.net/)でAjax通信を行う方法をまとめます。
以降のサンプルプログラムではDjangoを使用しています。

DataTablesの設置

まず、次のテーブルをWebページに設置します。
デザインにはBootstrapを使用しています。

f:id:hiroki-sawano:20181121002412p:plain

table 要素の idmytable とし、ヘッダのみ作成しておきます。

<table id="mytable" class="table table-striped table-bordered table-hover dataTable no-footer dtr-inline">
    <thead>
        <tr>
            <th>Column1</th>
            <th>Column2</th><th>Column10</th>
        </tr>
    </thead>
</table>

続けて、DataTableの初期化を行います。
$(document).ready() で各種設定を行います。

$('#mytable').DataTable({
  scrollX: true,
  ordering: false,
  displayLength: 25,
  deferRender: true,
  ajax: {
    "processing": true,
    "url": "{% url 'myapp:action' %}",
    "data": function( d ) {
      d.param1 = $('#id_param1').val();
      d.param2 = $('#id_param2').val();
      d.param3 = $('#id_param3').val();
    },
    "dataSrc": ""
  },
  columns: [
    { "data": "item1",
      "render": function (data, type, full, meta) {
        return '<a href="/link/to/another/page?param=' + full.item1 + '">' + full.item1 + '</a>';
      }
    },
    { "data": "item2" },
    { "data": "item3" },
    { "data": "item4" },
    { "data": "item5" },
    { "data": "item6" },
    { "data": "item7" },
    { "data": "item8" },
    { "data": "item9" },
    { "data": "item10" }
  ]
});

ajax オプション

url ではリクエスト先のURLを指定します(例ではDjangoテンプレートの記法で記述)。
data ではGET時のパラメータを設定しています。

columns オプション

DataTableの各列に表示する値を設定します。
基本的な使い方として{ "data": "key" }を列の数だけ用意しておき、 keyJSONデータのキーを指定します。 例えば [{"key": 100}] というデータがサーバから返却されたとき、 対象の列には100が設定されます。
単純に値を出力するだけでなく、例えばその値を用いてハイパーリンクを構成したり、 画像へのリンクを生成したい場合には、前述の例に示したように render オプションを使用します。

deferRender オプション

多量データを処理する場合には deferRender: true を設定すると性能が向上します。
このオプションを有効にすることでデータテーブルは実際に画面表示に必要な要素のみを読み込むため、 数万件のレコードを受信した場合でも、ページングの都度必要な件数のみを処理することができます。

Ajax通信

f:id:hiroki-sawano:20181121012614g:plain

クライアントサイド

myapp:action へのGETリクエストで設定するパラメータを入力するフォームを設置します。

<form id="myform" role="form">
  <div class="form-group">
    <label>Param1</label>
    <input type="text" name="column1" class="form-control" id="id_param1">
  </div>
  <div class="form-group">
    <label>Param2</label>
    <input type="text" name="column2" class="form-control" id="id_param2">
  </div>
  <div class="form-group">
    <label>Param3</label>
    <input type="text" name="column3" class="form-control" id="id_param3">
  </div>
  <button type="submit" class="btn btn-default">Submit</button>
</form>

Submit ボタン押下時のコールバックでは DataTable().ajax.reload() によってサーバとのAjax通信を開始します。

$("#myform").submit(function(event) {
  event.preventDefault();
  $('#mytable').DataTable().ajax.reload();
  return false;
});

このときデータテーブルでは上記テキストボックスに入力した値を クエリストリングに設定しています。

従ってURLは /myapp/action?param1=foo&param2=bar&param3=baz のような形式になります。

(再掲)

  ajax: {"data": function( d ) {
      d.param1 = $('#id_param1').val();
      d.param2 = $('#id_param2').val();
      d.param3 = $('#id_param3').val();
    },
    …
  },

medium.com

サーバサイド

サーバサイドの実装は今回主題ではないため、簡単に固定のJSONを返却しておきます。
ここで param1 の設定がない場合をページロード時の通信であると判断して空のJSONを返却しています。
どうやら serverSide : truedeferLoading : 0 にすると初回のAjax通信を抑止できるようですが試してません。

def action(request):
    if request.GET.get("param1"):
        response = '[{"item1":1, "item2":2, "item3":3, "item4":4, "item5":5, "item6":6, "item7":7, "item8":8, "item9":9, "item10":10}]'
        return HttpResponse(response, content_type='application/json')
    else:
        # Request on page load
        return HttpResponse('{}', content_type='application/json')

stackoverflow.com

Ajaxリロード時のプログレス表示

processing : true であるにも関わらず、 DataTable().ajax.reload()Loading... が表示されない問題が生じたため次のコードで対処しました。

$('#mytable').on('preXhr.dt', function(e, settings, data){
  $(this).DataTable().clear();
  settings.iDraw = 0;
  $(this).DataTable().draw();
});

stackoverflow.com

エラー処理

Ajax通信のエラー処理を実装する場合には、 xhr.dt イベントでHTTPステータス xhr.status を判定し、 サーバサイドから返却された xhr.responseJSON 内のエラーメッセージ等を表示、 DataTable().clear().draw() で空のデータテーブルを再描画すると良さそうです。

$('#mytable').on('xhr.dt', function ( e, settings, json, xhr ) {
  // Receive BadRequest status
  if (xhr.status == '400') {
    // Show error message
    $("#err_msg").text(xhr.responseJSON.error[0].message);
    $("#err_msg").show();
    // Redraw data table
    $('#mytable').DataTable().clear().draw();
    // Prevent error.dt from occurring
    return true;
  }
});

qiita.com

(2020/07/26追記) ヘッダ・ボディ間のスペース除去

scrollX: true を設定するとヘッダとボディの間にスペースが生じます。
DataTableで scrollCollapse: true を設定し、以下のスタイルを適用すると除去できます。

$('#mytable').DataTable({
  scrollX: true,
  scrollCollapse: true, // 追加
  // ...
<style>
.dataTables_scrollBody thead {
    visibility: collapse !important;
}
.dataTables_scrollBody {
  margin-top: -20px;
}
</style>

stackoverflow.com

VimでNeomake + Pylavaを使用したPython3の自動静的コード解析

はじめに

Python3のコーディングをVimで行う際にPEP8規約違反などを自動的に検出する方法をまとめます。

静的検証ツール Pylava の導入

Pythonの静的解析ツール Pylava を使用します。

github.com

Pylava では pycodestylePyFlakes などのツール群が利用可能であるため強力なコードチェックが可能です。
同様のツールとして Pylama がありますが、現状Python3.7に未対応です。
そこで Pylama からフォークした Pylava を選択しました。

github.com

github.com

Pylava では例えば次のような検証ができます。

  • pycodestyle : コードのPEP8規約準拠
  • pydocstyle : docstring のPEP257規約準拠
  • pyflakes : 論理的なエラー検知(未使用のモジュールや未定義の名前など)
  • Maccabe : McCabeの複雑度

インストール

$ pip install pylava

Pylavaの設定

pylava は実行時にカレントディレクトリから設定ファイルを探します。
pylava.ini を作成することで使用するlinterや無視する規約などを指定することができます。 pydocstyle はデフォルトだと無効なので明示的に記述する必要があります。

pylava.ini

[pylava]
skip = */settings.py
linters = pycodestyle,pydocstyle,pyflakes,mccabe
ignore = D203,D213,D401,D406

個人設定では ignore には以下のエラーコードを指定しています。
PEP257では規約内で矛盾が生じている箇所があるため個人の好みやプロジェクト内のルールに従い選択が必要です。

エラーコード エラーメッセージ
D203 1 blank line required before class docstring
D213 Multi-line docstring summary should start at the second line
D401 First line should be in imperative mood
D406 Section name should end with a newline

Pylavaの動作確認

例として次のサンプルプログラムを用意します。
使用していない sys のimportが行われている等の不備に気付くかと思います。

sample.py

import sys

def plus(a, b):
    print(a + b)

plus(1,2)

sample.py は1と2を足して3を出力するだけの単純なプログラムです。

$ python sample.py 
3

このままでも実行可能ですが、 pylava を実行することで改善点が多々あることが検知できます。

$ pylava
sample.py:1:1: D100 Missing docstring in public module [pydocstyle]
sample.py:1:1: W0611 'sys' imported but unused [pyflakes]
sample.py:3:1: E302 expected 2 blank lines, found 1 [pycodestyle]
sample.py:3:1: D103 Missing docstring in public function [pydocstyle]
sample.py:6:1: E305 expected 2 blank lines after class or function definition, found 1 [pycodestyle]
sample.py:6:7: E231 missing whitespace after ',' [pycodestyle]

VimでPylavaを自動実行

Neomakeの導入

コーディング後に都度 pylava を実行するのは手間なのでVim上で自動的にエラー検知を可能にします。 pylavaの自動実行にはVimプラグイン neomake を使用します。

github.com

dein.toml

[[plugins]]
repo = 'neomake/neomake'

Neomakeの設定

Neovimの場合は .init.vimVimの場合は .vimrcNeomake から Pylava を使用するための設定を記述します。

.init.vim(.vimrc)

call neomake#config#set('ft.python.pylama.exe', 'pylava')
call neomake#configure#automake('nrwi', 100)
let g:neomake_open_list = 2
let g:neomake_python_enabled_makers = ['pylama']
let g:neomake_python_pylama_maker = {
        \ 'args': ['--format', 'parsable', '-o', '~/pylava.ini'],
        \ 'errorformat': '%f:%l:%c: [%t] %m',
        \ 'postprocess': function('neomake#makers#ft#python#PylamaEntryProcess'),
        \ 'output_stream': 'stdout',
        \ 'exe': '/usr/local/bin/pylava'
        \ }

特に注意が必要な点として、 pylava を 使用するためには次の設定が必要です。

.init.vim(.vimrc)

call neomake#config#set('ft.python.pylama.exe', 'pylava')

github.com

また前述の pylava.iniVimからも共有したいので、 args には pylava -o ~/pylava.ini を指定しています(設定ファイルのパスは適宜変更してください)。

.init.vim(.vimrc)

let g:neomake_python_pylama_maker = {
        \ 'args': ['--format', 'parsable', '-o', '~/pylava.ini'],
        ...

exe には pylava のパスを指定します。
conda などで導入してある pylava を使用したい場合にもこちらで指定が可能です。

.init.vim(.vimrc)

let g:neomake_python_pylama_maker = {
        ...
        \ 'exe': '/usr/local/bin/pylava'
        \ }

エラーの一覧はロケーションリストに出力されるため、 エラー箇所への移動を簡単にするためにマッピングを設定します。

.init.vim(.vimrc)

map <C-n> :lnext<CR>
map <C-p> :lprev<CR>

動作確認

このようにしてVimから離れることなくコードの質を高めることができます。

f:id:hiroki-sawano:20181111145649g:plain