Thor x Rails 6.0での注意点

Rails アプリケーションを構築する際に、バッチ処理や便利タスクなどで Thor (現在の最新バージョンは 1.0.1) を使っている機会があると思うのですが、Rails 5.2 から Rails 6.0 で挙動が変わっていた点とその解決方法をシェアしておきます。

対象

Thor タスクのなかで options.except(:key) を使っているコードが対象です。

class Foo < Thor
  option ...
  def foo
    options.except(:key) # これ
  end
end

言い換えると Thor で options.except を使いその引数がシンボルでなければ、この問題はありません。

現象

options のクラスである Thor::CoreExt::HashWithIndifferentAccessexcept メソッドの挙動が変わっています。

Rails 5.2 まで

h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"bar"=>2}

Rails 6.0

h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"foo"=>1, "bar"=>2}

なので options.except の引数にシンボルでキー指定しても except されていない結果が返ります。

原因

https://github.com/rails/rails/pull/35771 のパフォーマンスチューニングの影響を受けてしまっていたようです。

解決

3つ例示しておきます。

1つめは場所が限られているようであれば、options.except('key') のように引数をシンボルから文字列にするのが分かりやすいワークアランドです。ただ upstream で予期されていないであろう挙動の影響でプロダクトコードに手入れするのがちょっとくやしい。

2つめは例えば config/initializers/thor.rb のようなファイルで以下のようなモンキーパッチを適用する手段です。

class Thor
  module CoreExt
    class HashWithIndifferentAccess
      def except(*keys)
        dup.tap do |hash|
          keys.each { |key| hash.delete(convert_key(key)) }
        end
      end
    end
  end
end

3つめ。以下のパッチを開いているのでマージされてリリースされるまで Gemfile に github 指定するなどできます。

github.com

昨晩直して先ほど PR を開いたばかりなので、これから動きがあることを期待しています。

Rails アップグレード時などの参考にどうぞ。

不正なバイトを置換文字で置き換えるCSV.openオプション

CSV 3.1.6 がリリースされました。

github.com

CSV 3.1.6 には、CSV.open に不正なバイトを置換文字で置き換えるオプションを追加するのに送ったパッチが取り込まれているのでその紹介です。

ユースケースとして、MS Excel 向けのエンコーディング (有名な CP932) に変換して CSV 出力するに際して、例えば Rails アプリケーションのバリデーション不足などで含まれてしまった不正なバイトをハンドリングしたいケースに活用できるオプションです。

CSV.open(..., invalid: :replace)

指定しているエンコーディング (ここでは CP932) へ変換できないバイトが含まれている場合、不正なバイトを置換文字で置き換えるという:invalid => :replace オプションが File.open にあります。CSV 3.1.5 までは CSV.open では受け付けないオプションだったため、例えば以下のように指定する必要がありました。

File.open(filename, 'w', encoding: Encoding::CP932, invalid: :replace, undef: :replace) do |file|
  csv = CSV.new(file, encoding: Encoding::CP932)

  csv << ...

  csv.close
end

CSV 3.1.6 から CSV.open:invalud オプションを受け付けるようになったため、以下のように書くことができます。

CSV.open(filename, 'w', encoding: Encoding::CP932, invalid: :replace) do |csv|
  csv << ...
end

対応したパッチは以下です。

github.com

また、Encoding::InvalidByteSequenceError が起きるようなエンコーディングに対して invalid: :replace オプション指定した場合、期待に反して ArgumentError が起きる以下のようなコードの問題についても解消されています。

require 'csv'

filename = 'foo.csv'

File.open(filename, 'w', encoding: Encoding::CP932, invalid: :replace) do |file|
  CSV.open(filename, encoding: Encoding::CP932) do |rows|
    rows << ["\x82\xa0"]
  end
end

これは上記のようなコードを書いていたとしても CSV 3.1.5 までは以下のエラーになっていたものです。

% ruby /tmp/csv.rb
Traceback (most recent call last):
        10: from /tmp/csv.rb:5:in `<main>'
         9: from /tmp/csv.rb:5:in `open'
         8: from /tmp/csv.rb:6:in `block in <main>'
         7: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv.rb:658:in `open'
         6: from /tmp/csv.rb:7:in `block (2 levels) in <main>'
         5: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv.rb:1230:in `<<'
         4: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:46:in `<<'
         3: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:46:in `collect'
         2: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:47:in `block in <<'
         1: from /Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:159:in `quote'
/Users/koic/.rbenv/versions/2.7.1/lib/ruby/2.7.0/csv/writer.rb:159:in `match?': invalid byte sequence in UTF-8 (ArgumentError)

対応したパッチは以下です。

github.com

CSV.open(..., undef: :replace)

こちらも CSV 3.1.6 で以下のように File.openundef: :replace オプションを CSV.open に指定することができるようになったので、簡潔に対応できるようになりました。

CSV.open(filename, 'w', encoding: Encoding::CP932, undef: :replace) do |csv|
  csv << ...
end

対応したパッチは以下です。

github.com

CSV gem は Gemify されているため、Ruby のアップデートと独立して bundle update できます。詳しくは以下のエントリを参照してください。

koic.hatenablog.com

ruby/csv にパッチを送った際にスピーディーなレビューをしていただいた須藤さんありがとうございました。

パーフェクトRuby on Rails【増補改訂版】をレビュアー献本いただいた

パーフェクトRuby on Rails【増補改訂版】をレビュアー献本いただいた。出版おめでとうございます & 献本ありがとうございました。

以前、Ginza.rb のおりに netwillnet さんから「パ Rails の改訂版を書くにあたってレビュアーお願いできますか?」と声をいただいて、執筆パート担当の igaiga555 さんからのパスでレビュアーに加わらせていただいていた。

全体については仕事の速い tatsuoSakurai さんが書いている書評を見ていただくとして、RuboCop に関わるパートで出版後の変化について記しておこうと思う。

本編9章で rubocop -a を実行すると Style/FrozenStringLiteralComment cop によるマジックコメントが適用されるとあるのですが、RuboCop 0.87 以降では -a オプションでは適用されなくなりました。rubocop -a は安全な cop のみ自動修正するオプションという意味合いに変わったためです。

そのため安全でないとマークされている Style/FrozenStringLiteralComment cop を使った自動修正をするには rubocop -A (大文字の A ) を実行する必要があります (この変更が取り込まれたのは、おそらく脱稿して原稿が印刷所に行っているころ) 。

安全でない自動修正とは、例えば 'foo' << 'bar' のようなコードがあった場合に、frozen string リテラルのマジックコメントなしからありになると、FrozenError (can't modify frozen String) という挙動に変わりますが、このように互換性がない auto-correct になりうるものです (このケースで互換性を維持するためにはレシーバーを dup などする必要がある) 。

rubocop -arubocop -A について詳しくは以下のエントリを参照してください。

koic.hatenablog.com

技術を扱う書籍についてツールがアップデートされていくのは常ですが、レビュー時点ではこの変更への PR 自体も出ていなかったので 1ヶ月くらい時期がずれていれば ...といったライブラリアップデートに関する補完となります。

本書に戻ると Rails 6.0 対応で Docker や GitHub Actions にも触れられているので、これからの業務への知識のステップアップを目指す方などに適した一冊になるのではと思います。改めて執筆お疲れ様でした!

rubocop -a と rubocop -A オプション

RuboCop 0.87 がリリースされた。

github.com

今回の目玉は rubocop -a コマンドラインオプションへの非互換変更となる。

これまでは rubocop -a オプション (rubocop --auto-correct も同義) を使った場合に、自動修正を備えたすべての Cop が適用されたていた。そのため Safe でないと見なされる Cop の自動修正も適用されるというのがデフォルトの振る舞いだった。

今回のリリースからは rubocop -a は Safe とマークされている Cop のみが自動修正の対象となり、従来どおり自動修正を備えた Unsafe を含むすべての Cop を適用する場合は rubocop -A (rubocop --auto-correct-all) を実行することになる。つまり RuboCop 0.59 で導入された rubocop --safe-auto-correct が自動修正のデフォルトの振る舞いになる (そして rubocop --safe-auto-correct オプション自体は非推奨になりました) 。

RuboCop に導入されている Safe というメタデータについては過去の日記を参照してください。

koic.hatenablog.com

GitHubの新デザインを先取りして使う

GitHub がユーザー全体のデザインを変えるより前に、Feature preview を使うと先駆けで新デザインを使うことができる。

先行して新デザインが使えるときは、以下の Feature preview に通知がきているので、それを有効にする。

f:id:koic:20200624175208p:plain:w400

過去に自分が見たプレビュー機能には、"Design updates" と "Repository refresh" があった。

"Design updates" はユーザープロフィールページを新デザインにするものだった。

"Repository refresh" はリポジトリのトップページを新デザインにするものだった。

それぞれ "Give feedback" のリンクからフィードバックできるようになっている。

"Repository refresh" について、リポジトリのトップに表示されている最新のコミットで CI の結果 (✔︎or❌) が分からくなっていて不便だったのを以下の画像添付と一緒に GitHub にフィードバックしていた (たしか昨日 (2020年6月23日) か一昨日くらいのこと) 。

f:id:koic:20200624174907p:plain:w400

この日記を書いている 2020年6月24日現在、リポジトリのトップで master ブランチへのマージ後の最新のコミットへの CI の結果がわかるようになっていた。GitHub への自分のフィードバックか、誰か他の人も同一のフィードバックをしていたかは分からないが、便利になって良かった (というかもともとのデザインで見れていたものなので、ないと不便だった) 。

いまは "Feature preview" が空になっているので、また次のプレビューが楽しみですね。

GitHub が落ちた時に復旧情報を受け取る

Twitter のタイムラインが賑わう GitHub のサービスダウンに関する tip です。

f:id:koic:20200619184749p:plain:w400

GItHub のサービスの状態については https://status.github.com/ で確認できます。

f:id:koic:20200619184810p:plain:w400

さらに右上の Subscribe から復旧状態をメールなどで通知を受け取ることができます。

f:id:koic:20200619184827p:plain:w400

たとえばメール通知にすると、復旧したらこんな感じでメールが来ます。

f:id:koic:20200619185820p:plain:w400

実践的には GitHub に繋がらなければ繋がらないでできる仕事にスイッチして進めている間にだいたい復旧しているので、UX 最高とまではいかなかったりですが参考までに。

pluck.uniqをdistinct.pluckに置換する

先日リリースした RuboCop Rails 2.6.0 でバグフィックスされた Rails/UniqBeforePluck についての話。

この Cop は RuboCop Rails に細かな部署があるなら、スタイルではなくパフォーマンスに属するもの。RDBMS から結果を取得したあとに Rubyuniq で一意にするのではなく、DISTINCT を使ったクエリを RDBMS に発行してあらかじめ一意になっている結果を得るもの。

デフォルトで以下のような検出を行う。

# bad
Model.pluck(:foo).uniq

# good
Model.distinct.pluck(:foo)

なお、この Rails/UniqBeforePluck cop はバグ修正された RuboCop Rails 2.6 以上でないと distinct に置換すべきところ Rails 5.0 で非推奨にされた uniq にオートコレクトしてしまう問題がある。

github.com

いまメンテナンスされている Rails 5.2, Rails 6.0 なんかでは NoMethodError: undefined methoduniq'` エラーになるので自動修正を使うときは RuboCop Rails を最新の 2.6.0 に更新してください。

なお Rails/UniqBeforePluckEnforcedStyle のオプションを持っていて、デフォルトの EnforcedStyle: conservative偽陰性が起きえて、EnforcedStyle: aggressive偽陽性が起きうるという。常時はデフォルトの EnforcedStyle: conservative のままとしている。パッチ会あたりでも聞いてみたいところ。

github.com あとは悲しいかな、誤検知をもちうる Cop のため git grep 'pluck.*uniq' なんかで置き換えが可能かどうかを見て判定というのが最初の導入として確実。まずは grep で対応した後に EnforcedStyle: conservative あたりで検出を行うというのが、まさに consavative なやり方になるだろう。型があれば誤検知なく EnforcedStyle: conservative が使えそうで未来の処理系に期待される。

そして、Rails/UniqBeforePluck cop は歴史的経緯ですでに名前が実体にあっていないので、RuboCop Rails 3.0 あたりでリネームされる予定です。

追記

同僚の kunitoo より例えば MySQL の collation によって、RDBMS によっては非互換が生まれる可能性があるかもという指摘があった。たしかに現状の実装だとオートコレクトの unsafe を検討できるかもしれない。