2022年10月31日月曜日

pytestで抽象クラスをテストする方法

pytestで抽象クラスをテストする方法

概要

いつもやり方を忘れるのでメモ

Hoge.__abstractmethods__ = set() してからクラスを作成してそのクラスでテストする

環境

  • Ubuntu 18.04
  • Python 3.10.2

サンプルコード

class Hoge():
    def say(self):
        raise NotImplementedError()
  • vim test_app.py
import pytest

from app import Hoge


class TestHoge():

    Hoge.__abstractmethods__ = set()

    def test_hoge(self):

        class RealHoge(Hoge):
            pass

        rh = RealHoge()
        with pytest.raises(NotImplementedError):
            rh.say()

2022年10月28日金曜日

emacs の lsp-mode で取り込んだワークスペースのルートディレクトリの設定をクリアする方法

emacs の lsp-mode で取り込んだワークスペースのルートディレクトリの設定をクリアする方法

概要

lsp-mode でプロジェクトなどを追加して初回にプロジェクト内のファイルを読み込むときにプロジェクトのルートディレクトリを指定することができます
しかし Ctrl+g などでキャンセルしてしまうことがあります
その場合の対処方法を紹介します

環境

  • macOS 11.6.8
  • emacs 28.1
  • lsp-mode 20220201.852

対処方法

  • rm ~/.emacs.d/.lsp-session-v1

参考サイト

2022年10月27日木曜日

pypiから取得したパッケージをローカルに持ってきてローカルのパッケージを参照するように変更する方法

pypiから取得したパッケージをローカルに持ってきてローカルのパッケージを参照するように変更する方法

概要

ローカルでパッケージを開発したい場合に Pipfile の参照先を Pypi ではなく local に変更する方法を紹介します

環境

  • Ubuntu 18.04
  • Python 3.10.2
  • pipenv 2022.1.8

既存パッケージの削除

pypi から取得したパッケージを一度削除します

  • pipenv uninstall hoge

Pipfile を編集してローカルからインストールするようにする

path を指定することでローカルからインストールできます

  • vim Pipfile
[packages]
hoge = {path="/home/pkg/hoge"}

再度インストール

あとは再度インストールします

  • pipenv install -d

もしパッケージ側を修正した場合は再インストール必要がある

パッケージ側を修正した場合は再度インストールする必要があります

もし面倒な場合はシンボリックリンクに手動で切り替えても OK です

  • pipenv --venv
  • cd /home/pkg/.local/share/virtualenvs/app-Yn-xfOlh/lib/python3.10/site-packages
  • rm -rf hoge
  • ln -s /home/pkg/hoge/hoge hoge

ただこの場合でもパッエージ側にパッケージが追加された場合は再度インストールする必要がある

パッケージ側で pipenv install や poetry add などが発生した場合は再度インストールし直しましょう

パッケージ側が poetry を使っている場合には editable モードは使えない

path を指定する際に editable というオプションが使えるのですがパッケージ側が poetry で作成されている場合以下のようなエラーになりました

hoge = {editable=true, path="/home/pkg/hoge"}
(A "pyproject.toml" file was found, but editable mode currently requires a setuptools-based build.)

2022年10月26日水曜日

FastAPIのミドルウェアを使ってみる

FastAPIのミドルウェアを使ってみる

概要

ミドルウェアはすべてのリクエストの前後に処理を入れることができる機能です

環境

  • macOS 11.6.8
  • Python 3.10.2
  • FastAPI 0.83.0

サンプルコード

import time

from fastapi import FastAPI, Request

app = FastAPI()


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response


@app.get("/")
async def get_msg():
    return "Hello"

ちょっと解説

@app.middleware("http") でミドルウェアを定義します
このデコレーションが付いているメソッドが各リクエストの前 or 後に呼ばれるようになります

call_next が呼ばれるとルーティングの本体の処理が実行されるのでその前後でやりたいことを記載します

デコレータでミドルウェアを使う場合は http を指定するのが必須になります
その他標準で備わっているミドルウェアもあるのでそれらを使う場合は app.add_middleware を使います (参考)

参考サイト

2022年10月25日火曜日

FastAPIでURLのバリデーションをpydanticを使って行う方法

FastAPIでURLのバリデーションをpydanticを使って行う方法

概要

カスタムリクエスト+カスタムルーティングと組み合わせて実現します

環境

  • macOS 11.6.8
  • Python 3.10.2
  • FastAPI 0.83.0

サンプルコード

"""カスタムルートをテストするモジュール."""
import ast
import json

from pydantic import BaseModel, BaseSettings, validator

from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute

from typing import Callable


class Settings(BaseSettings):
    """一時的なデータ保存用のクラス."""

    message: str = ""
    hostname: str = ""


settings = Settings()


class RequestWithHostname(Request):
    """ホスト名をボディに必ず含むリクエスト."""

    async def body(self) -> bytes:
        """bodyメソッドのオーバライド."""
        if not hasattr(self, "_body"):
            body = await super().body()
            text_body = body.decode('utf-8')
            dict_body = ast.literal_eval(text_body)
            # url をセット
            url = self.url
            dict_body['hostname'] = url.hostname
            # bytes に再変換
            body = json.dumps(dict_body).encode('utf-8')
            self._body = body
        return self._body


class ContextIncludedRoute(APIRoute):
    """カスタムルートの定義."""

    def get_route_handler(self) -> Callable:
        """get_route_handlerのオーバライド."""
        # ルートハンドラの取得
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            """カスタムルートハンドラの定義.

            ここでルーティングごとに共通のロギングやヘッダのカスタマイズを行う.
            
            """
            request = RequestWithHostname(request.scope, request.receive)
            return await original_route_handler(request)

        # get_route_handlerは定義したカスタムルートハンドラを返却する
        return custom_route_handler


# アプリの作成とカスタムルートクラスの設定
app = FastAPI()
router = APIRouter(route_class=ContextIncludedRoute)


class Item(BaseModel):
    """pydanticを使ったリクエストモデル."""

    message: str
    hostname: str

    @validator('hostname')
    def is_fuga(cls, v, values, **kwargs):
        """ホスト名をチェック."""
        if v != 'fuga':
            raise ValueError('Hostname must be fuga')
        return v


# 定義したrouterを元に各種ルーティングを定義
@router.get("/")
async def get_msg():
    """メッセージを取得."""
    return {"message": settings.message, "hostname": settings.hostname}


@router.post("/")
def set_msg(item: Item):
    """メッセージの登録."""
    settings.message = item.message
    settings.hostname = item.hostname
    return {"message": item.message, "hostname": item.hostname}


# 定義したルーティングをアプリに登録する
app.include_router(router)

ポイント

  • カスタムクラス+カスタムルーティングを使ってボティの要素を追加する
  • URLやホスト名の情報は Request オブジェクトから取得する
  • カスタムクラスを扱う場合はボディ情報が bytes 型なので型変換が必要になる

実行

  • pipenv run uvicorn app:app --reload

成功

  • curl -v -XPOST localhost:8000 -H 'Host: fuga' -H 'content-type: application/json' -d '{"message":"hoge"}'

失敗

  • curl -v -XPOST localhost:8000 -H 'Host: fuga2' -H 'content-type: application/json' -d '{"message":"hoge"}'

2022年10月24日月曜日

FastAPIでリクエストとレスポンスのロギングをする場合はカスタムルートを使うのが良さそう

FastAPIでリクエストとレスポンスのロギングをする場合はカスタムルートを使うのが良さそう

概要

最初、各ルーティングの共通処理なのでミドルウェアがいいかなと思っていたのですがリクエストボディが扱えないようです
今回はカスタムルートを使ってリクエストとレスポンス情報をロギングする方法を紹介します

環境

  • macOS 11.6.8
  • Python 3.10.2
  • FastAPI 0.83.0

サンプルコード

  • vim app.py
"""カスタムルートをテストするモジュール."""
from pydantic import BaseModel
from pydantic import BaseSettings

from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute

from typing import Callable
from uuid import uuid4


class Settings(BaseSettings):
    """一時的なデータ保存用のクラス."""

    message: str = ""


settings = Settings()


class ContextIncludedRoute(APIRoute):
    """カスタムルートの定義."""

    def get_route_handler(self) -> Callable:
        """get_route_handlerのオーバライド."""
        # ルートハンドラの取得
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            """カスタムルートハンドラの定義.

            ここでルーティングごとに共通のロギングやヘッダのカスタマイズを行う.
            
            """
            request_id = str(uuid4())
            # レスポンスオブジェクトの取得
            response: Response = await original_route_handler(request)
            # リクエストボディのロギング
            if await request.body():
                print(await request.body())
            # カスタムヘッダ Request-ID の登録
            response.headers["Request-ID"] = request_id
            return response

        # get_route_handlerは定義したカスタムルートハンドラを返却する
        return custom_route_handler


# アプリの作成とカスタムルートクラスの設定
app = FastAPI()
router = APIRouter(route_class=ContextIncludedRoute)


class Item(BaseModel):
    """pydanticを使ったリクエストモデル."""

    message: str

# 定義したrouterを元に各種ルーティングを定義
@router.get("/")
async def get_msg():
    """メッセージを取得."""
    return {"message": settings.message}


@router.post("/")
def set_msg(item: Item):
    """メッセージの登録."""
    settings.message = item.message
    return {"message": item.message}


# 定義したルーティングをアプリに登録する
app.include_router(router)

ちょっと解説

基本はコード内のコメントに記載しています

ポイントは APIRoute を継承したクラスを定義し get_route_handler をオーバライドする点です
その返り値に自分で定義したハンドラメソッドをリターンするだけです

ハンドラオブジェクトからリクエストからレスポンスを取得、操作できるのでそれを元にロギングやカスタマイズを行います

カスタムルートを元にアプリを作成します
作成したアプリからルーティング定義用のオブジェクトを作成してルーティングを定義します
最後に定義したルーティング情報をアプリに登録すればカスタムルートが適用されたアプリが起動します

参考サイト

2022年10月21日金曜日

FastAPIでPOSTリクエストのボディをバリデーションする方法

FastAPIでPOSTリクエストのボディをバリデーションする方法

概要

前回 FastAPIに入門しました

今回はリクエストのバリデーションを実装する方法を紹介します

環境

  • macOS 11.6.8
  • Python 3.10.2
  • FastAPI 0.83.0

pydandic の @validator を使う

おそらく一番メジャーでかつ簡単なのが pydantic の validator デコレータを使う方法だと思います
FastAPI は pydantic と密接に関係しており主にリクエストのモデルを扱う場合に使用する場合が多いです
なのでバリデーションも pydantic の機能を使うのが良いと思います

from pydantic import BaseModel, BaseSettings, validator
from fastapi import FastAPI

app = FastAPI()


class Settings(BaseSettings):
    message: str = ""


settings = Settings()


class Item(BaseModel):
    message: str

    @validator('message')
    def check_max_length(cls, v, values, **kwargs):
        if len(v) > 10:
            raise ValueError('Must be 10 characters or less.')
        return v

    @validator('message')
    def contain_exclamation(cls, v, values, **kwargs):
        if '!' not in v:
            raise ValueError('Must contain a exclamation mark')
        return v

@app.get("/")
async def get_msg():
    return {"message": settings.message}


@app.post("/")
def set_msg(item: Item):
    settings.message = item.message
    return {"message": item.message}

解説

@validator('message') がポイントです
BaseModel を継承したリクエストモデル用のクラスに実装します
引数の値はバリデーションを行うフィールドの値を指定します
同じフィールドい対してバリデーションのメソッドはいくつも登録することができるのでバリデーションの種類に応じてわけると良いかなと思います

ただバリデーションルールはOpenAPIに反映されない

これで /openapi.json を見るとわかるのですがバリデーション情報は openapi.json には反映されていません

Body を使う

pydantic を使わずにリクエストボディを定義することもできます (pydantic と組み合わせることもできます)
Body を使って定義することで openapi にもバリデーション情報が反映されます

from pydantic import BaseSettings
from fastapi import FastAPI, Body

app = FastAPI()


class Settings(BaseSettings):
    message: str = ""


settings = Settings()


@app.get("/")
async def get_msg():
    return settings.message


@app.post("/")
def set_msg(message: dict[str, str] = Body(max_length=10, regex=".*!.*", example={"message": "Hello!"})):
    settings.message = message
    return message

Body の他に指定可能なパラメータは参考サイトのURLを参照してください

動作確認

  • pipenv run uvicorn app:app --reload

で起動します

  • curl -XPOST localhost:8000 -H 'content-type: application/json' -d '{"message":"hoge!"}'

は成功します

  • curl -XPOST localhost:8000 -H 'content-type: application/json' -d '{"message":"hoge"}'
  • curl -XPOST localhost:8000 -H 'content-type: application/json' -d '{"message":"hogehogehoge!"}'

はエラーになることが確認できると思います

参考サイト

2022年10月20日木曜日

FastAPIでDependsを使ってパスパラメータのバリデーションにpydanticを使う方法

FastAPIでDependsを使ってパスパラメータのバリデーションにpydanticを使う方法

概要

FastAPIのパスパラメータに pydantic の BaseModel を扱う方法を紹介します

ポイントは Depends を使う点です

環境

  • macOS 11.6.8
  • Python 3.10.2
  • FastAPI 0.83.0

サンプルコード

from fastapi import Depends, FastAPI, Path
from pydantic import BaseModel, validator, Field

app = FastAPI()


async def common_parameters(q: str | None = None,
                            skip: int = 0,
                            limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


class Item(BaseModel):
    msg: str = Field(Path('msg'))

    @validator('msg')
    def check_max_length(cls, v, values, **kwargs):
        if len(v) > 10:
            raise ValueError('Must be 10 characters or less.')
        return v


@app.get("/{msg}")
async def read_items(commons: dict = Depends(common_parameters),
                     msg: Item = Depends(Item)):
    return commons

ちょっと解説

パスのモデルクラスは Item としています
そのフィールドでパスパラメータとして来る値を定義します
その際に Field と Path を使うことでパスパラメータに来る値を定義できます msg: str = Field(Path('msg'))

あとは定義したモデルクラスをパスパラメータの型ヒントとしてルーティン側で指定します
その際にデフォルト値として Depends(Item) とすることでモデルクラスを利用することができるようになります

もし Depends がないと AssertionError: Path params must be of one of the supported types エラーになります

動作確認

  • pipenv run uvicorn app:app --reload

成功

  • curl -v -XGET 'localhost:8000/hello'

失敗

  • curl -v -XGET 'localhost:8000/hellohellohello'

参考サイト

2022年10月19日水曜日

Gitlabのパッケージレジストリを使ってpipenv install する場合はバージョンに注意しなければならない

Gitlabのパッケージレジストリを使ってpipenv install する場合はバージョンに注意しなければならない

概要

pipenv が最新だと動作しない可能性があります

環境

  • Gitlab-ee 15.2.4
  • Python 3.10.2
  • pipenv 2022.1.8

pipenv のバージョンを指定してインストール

  • pip install --user pipenv==2022.1.8

2022年10月18日火曜日

TypeScriptで環境変数を使う方法

TypeScriptで環境変数を使う方法

概要

process.env を使います

環境

  • macOS 11.6.8
  • nodejs 18.7.0
  • TypeScript 4.8.3

準備

  • npm install ts-node typescript
  • npx tsc --init

index.ts

console.log(process.env.USER_NAME || 'hawksnowlog');

動作確認

  • npx ts-node index.ts

最後に

デフォルト値は OR 演算子で設定できます

2022年10月17日月曜日

QuasarでAjaxリクエストを送信する方法

QuasarでAjaxリクエストを送信する方法

概要

axios を使って Quasar で外部のサイトにリクエストを送信する方法を紹介します

環境

  • macOS 11.7
  • node 18.9.0
  • quasar 2.8.2
  • axios 0.27.2

インストール

Quasarプロジェクト作成時にインストールしなかった場合は別途インストールします

  • yarn add axios

(任意)通知の有効化

$q.notify を使うので設定ファイルを編集して通知を有効にします
quasar.config.js というファイルがありその中の framework という設定箇所があるのでそこの plugins と config を変更します

plugins には「Notify」を入力し config には「notify」を追加します

  • vim quasar.config.js
return {
  framework: {
    plugins: [
      'Notify'
    ],
    config: {
      notify: { /* look at QuasarConfOptions from the API card */ }
    }
  }
}

※上記は一部抜粋

サンプルコード

  • vim src/pages/IndexPage.vue
<template>
  <div>{{ data }}</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import axios from 'axios';
import { useQuasar } from 'quasar'

const data = ref('nodata')
const $q = useQuasar()

const api = axios.create({ baseURL: 'https://sample.allowed.cors.site' })

api.get('/')
  .then((response) => {
    data.value = response.data
  })
  .catch(() => {
    $q.notify({
      color: 'negative',
      position: 'top',
      message: 'Loading failed',
      icon: 'report_problem'
    })
  })
</script>

ちょっと解説

リクエストは axios を使って行います
今回は GET ですが POST なども行えます
今回のサンプルだとページを読み込んだ段階で毎回コールしてしまうのでボタンを押した場合やコンテンツが更新されたタイミングでコールするようにします

アクセスするサイトは当然ですが CORS 対応されたサイトにアクセスしてください
面倒であれば localhost に CORS 対応した適当のアプリを立ち上げても OK です

アクセスに成功すればその結果を data.value に設定します

2022年10月14日金曜日

Quasarでcookiesを扱う方法

Quasarでcookiesを扱う方法

概要

q-carousel の使い方をサンプルコード付きで紹介します
今回は q-btn-toggle を組み合わせて使いQuasar で Cookies を扱う方法を紹介します

環境

  • macOS 11.7
  • node 18.9.0
  • quasar 2.8.2

サンプルコード

  • vim src/pages/IndexPage.vue
<template>
  <q-btn label="Save" @click="saveCookie()" />
  <q-btn label="Clear" @click="clearCookie()" />
  <div>{{ userName }}</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Cookies } from 'quasar'

const cookieName = 'my_cookie_name'
const userName = ref(Cookies.get(cookieName))

const saveCookie = () => {
  const hasIt = Cookies.has(cookieName)
  if (!hasIt) {
    Cookies.set(cookieName, 'hawksnowlog')
  }
  userName.value = Cookies.get(cookieName)
}

const clearCookie = () => {
  Cookies.remove(cookieName)
  userName.value = Cookies.get(cookieName)
}
</script>

ちょっと解説

書き込む際は Cookies.set を使います
読み込む際は Cookies.get を使います
存在するかのチェックは Cookies.has を使います

なおオプションで期限やドメイン情報などを設定することができます

参考サイト

2022年10月13日木曜日

Quasarでfontawesomeを使う方法

Quasarでfontawesomeを使う方法

概要

q-carousel の使い方をサンプルコード付きで紹介します
今回は q-btn-toggle を組み合わせて使います

環境

  • macOS 11.7
  • node 18.9.0
  • quasar 2.8.2

quasar.conf.js の編集

extras という配列の項目があるのでそこの「fontawesome-v6」をコメントインします

  • vim quasar.config.js
    extras: [
      // 'ionicons-v4',
      // 'mdi-v5',
      'fontawesome-v6',
      // 'eva-icons',
      // 'themify',
      // 'line-awesome',
      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!

      'roboto-font', // optional, you are not bound to it
      'material-icons', // optional, you are not bound to it
    ],

q-icon で使う

あとは以下のように参照するだけです

<q-icon name="fa-brands fa-gitlab" />

サイズを指定する場合は size プロパティを使えば OK です

<q-icon name="fa-brands fa-gitlab" size="10em"/>

もしくは直接 vue ファイルで import しても OK

<template>
  <q-icon :name="fabGitlab" size="10em"/>
</template>

<script setup lang="ts">
import { fabGitlab } from '@quasar/extras/fontawesome-v6';
</script>

最後に

vue 用に font-awesome-icon というタグが用意されているのでそれを使っても良さそうな気がします

参考サイト

2022年10月12日水曜日

QuasarでQTableのexpandを使う方法

QuasarでQTableのexpandを使う方法

概要

特定のカラムの情報だけexpandで表示したい場合などに使えます
expand 後に更にテーブルを使う方法も紹介します

環境

  • macOS 11.7
  • node 18.9.0
  • quasar 2.8.2

サンプルコード

  • vim src/pages/IndexPage.vue
<template>
  <div class="q-pa-md">
    <q-table
      title="Treats"
      :rows="rows"
      :columns="columns"
      row-key="name"
    >

      <template v-slot:header="props">
        <q-tr :props="props">
          <q-th auto-width />
          <q-th
            v-for="col in props.cols"
            :key="col.name"
            :props="props"
          >
            {{ col.label }}
          </q-th>
        </q-tr>
      </template>

      <template v-slot:body="props">
        <q-tr :props="props">
          <q-td auto-width>
            <q-btn size="sm" color="accent" round dense @click="props.expand = !props.expand" :icon="props.expand ? 'remove' : 'add'" />
          </q-td>
          <q-td
            v-for="col in props.cols"
            :key="col.name"
            :props="props"
          >
            {{ col.value }}
          </q-td>
        </q-tr>
        <q-tr v-show="props.expand">
          <q-td
            colspan="100%"
            v-for="favorite in props.row.favorites"
            :key="favorite.id"
          >
            <q-chip color="primary" text-color="white" icon="edit">
              {{ favorite.color }}
            </q-chip>
            <q-chip color="red" text-color="white" icon="sports">
              {{ favorite.sports }}
            </q-chip>
            <q-chip color="green" text-color="white" icon="restaurant">
              {{ favorite.food}}
            </q-chip>
          </q-td>
        </q-tr>
      </template>

    </q-table>
  </div>
</template>

<script setup lang="ts">
const columns = [
  { name: 'name', required: true, label: 'name', align: 'left', field: 'name', sortable: true },
  { name: 'age', label: 'age', field: 'age', sortable: true },
]

const rows = [
  {
    id: 1,
    name: 'hawk',
    age: 10,
    favorites: [{'color': 'blue', 'sports': 'soccer', 'food': 'bread'}],
  },
  {
    id: 2,
    name: 'snowlog',
    age: 20,
    favorites: [{'color': 'red', 'sports': 'basketball', 'food': 'rice'}],
  },
  {
    id:3,
    name: 'taro',
    age: 30,
    favorites: [{'color': 'green', 'sports': 'tennis', 'food': 'meat'}],
  },
]
</script>

expand 後の更にQTableを使う方法

<template>
  <div class="q-pa-md">
    <q-table
      title="Treats"
      :rows="rows"
      :columns="columns"
      row-key="name"
    >

      <template v-slot:header="props">
        <q-tr :props="props">
          <q-th auto-width />
          <q-th
            v-for="col in props.cols"
            :key="col.name"
            :props="props"
          >
            {{ col.label }}
          </q-th>
        </q-tr>
      </template>

      <template v-slot:body="props">
        <q-tr :props="props">
          <q-td auto-width>
            <q-btn size="sm" color="accent" round dense @click="props.expand = !props.expand" :icon="props.expand ? 'remove' : 'add'" />
          </q-td>
          <q-td
            v-for="col in props.cols"
            :key="col.name"
            :props="props"
          >
            {{ col.value }}
          </q-td>
        </q-tr>
        <q-tr v-show="props.expand">
          <q-td
            colspan="100%"
          >
            <q-table
              title="Favorites"
              :rows="props.row.favorites"
              :columns="nextedColumns"
              row-key="id"
              flat
            >
            </q-table>
          </q-td>
        </q-tr>
      </template>

    </q-table>
  </div>
</template>

<script setup lang="ts">
const columns = [
  { name: 'name', required: true, label: 'name', align: 'left', field: 'name', sortable: true },
  { name: 'age', label: 'age', field: 'age', sortable: true },
]

const nextedColums = [
  { name: 'color', label: 'color', field: 'color' },
  { name: 'sports', label: 'sports', field: 'sports' },
  { name: 'food', label: 'food', field: 'food' },
]

const rows = [
  {
    id: 1,
    name: 'hawk',
    age: 10,
    favorites: [{'color': 'blue', 'sports': 'soccer', 'food': 'bread'}],
  },
  {
    id: 2,
    name: 'snowlog',
    age: 20,
    favorites: [{'color': 'red', 'sports': 'basketball', 'food': 'rice'}],
  },
  {
    id:3,
    name: 'taro',
    age: 30,
    favorites: [{'color': 'green', 'sports': 'tennis', 'food': 'meat'}],
  },
]
</script>

ちょっと解説

ポイントは入れ子にするオブジェクト専用のカラム情報を作成する点です
あとは値を渡すときに props.row.favarites という感じでexpandした際にだけ表示するカラムをループで渡してあげればOKです

また flat 属性を指定してあげるとテーブルが入れ子になってもキレイに表示されるのでおすすめです