{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a href=\"https://colab.research.google.com/github/RecoHut-Projects/recohut/blob/master/nbs/utils/config.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "!pip install -q nbdev\n",
    "!git clone https://github.com/RecoHut-Projects/recohut.git\n",
    "%cd recohut\n",
    "!pip install -q -e ."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp utils.config"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Config\n",
    "> A configurator module."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "from nbdev.showdoc import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "import os\n",
    "import sys\n",
    "from configparser import ConfigParser\n",
    "from collections import OrderedDict"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class Configurator(object):\n",
    "    \"\"\"A configurator class.\n",
    "    This class can read arguments from ini-style configuration file and parse\n",
    "    arguments from command line simultaneously. This class can also convert\n",
    "    the argument value from `str` to `int`, `float`, `bool`, `list` and `None`\n",
    "    automatically. The priority of arguments from command line is higher than\n",
    "    that from configuration file. That is, if there are same argument name in\n",
    "    configuration file and command line, the value in the former will be\n",
    "    overwritten by that in the latter. Moreover:\n",
    "    * Command line: The format of arguments is ``--arg_name=arg_value``,\n",
    "      there cannot be any space in the inner of an argument string.\n",
    "      For example::\n",
    "        python main.py --model=Pop --num_thread=128 --group_view=[10,30,50,100]\n",
    "    * Configuration file: This file must be ini-style. If there is only one\n",
    "      section and whatever the name is, this class will read arguments from\n",
    "      that section. If there are more than one sections, this class will read\n",
    "      arguments from the section named `default_section`.\n",
    "    After initialization successful, the objective of this class can be used as\n",
    "    a dictionary::\n",
    "        config = Configurator(\"./NeuRec.properties\")\n",
    "        num_thread = config[\"num_thread\"]\n",
    "        group_view = config[\"group_view\"]\n",
    "    Here, the types of `num_thread` and `group_view` are `int` and `list`,\n",
    "    respectively.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, config_file, default_section=\"default\"):\n",
    "        \"\"\"Initializes a new `Configurator` instance.\n",
    "        Args:\n",
    "            config_file (str): The path of ini-style configuration file.\n",
    "            default_section (str): The default section if there are more than\n",
    "                one sections in configuration file.\n",
    "        Raises:\n",
    "             FileNotFoundError: If `config_file` is not existing.\n",
    "             SyntaxError: If the format of arguments in commend line is invalid.\n",
    "             ValueError: If there is more than one section but no one section\n",
    "                named `default_section` in ini-style file.\n",
    "        \"\"\"\n",
    "        if not os.path.isfile(config_file):\n",
    "            raise FileNotFoundError(\"There is not config file named '%s'!\" % config_file)\n",
    "\n",
    "        self._default_section = default_section\n",
    "        self.cmd_arg = self._read_cmd_arg()\n",
    "        self.lib_arg = self._read_config_file(config_file)\n",
    "        config_dir = self.lib_arg[\"config_dir\"]\n",
    "        model_name = self.lib_arg[\"recommender\"]\n",
    "        arg_file = os.path.join(config_dir, model_name+'.properties')\n",
    "        self.alg_arg = self._read_config_file(arg_file)\n",
    "\n",
    "    def _read_cmd_arg(self):\n",
    "        cmd_arg = OrderedDict()\n",
    "        if \"ipykernel_launcher\" not in sys.argv[0]:\n",
    "            for arg in sys.argv[1:]:\n",
    "                if not arg.startswith(\"--\"):\n",
    "                    raise SyntaxError(\"Commend arg must start with '--', but '%s' is not!\" % arg)\n",
    "                arg_name, arg_value = arg[2:].split(\"=\")\n",
    "                cmd_arg[arg_name] = arg_value\n",
    "\n",
    "        return cmd_arg\n",
    "\n",
    "    def _read_config_file(self, filename):\n",
    "        config = ConfigParser()\n",
    "        config.optionxform = str\n",
    "        config.read(filename, encoding=\"utf-8\")\n",
    "        sections = config.sections()\n",
    "\n",
    "        if len(sections) == 0:\n",
    "            raise ValueError(\"'%s' is empty!\" % filename)\n",
    "        elif len(sections) == 1:\n",
    "            config_sec = sections[0]\n",
    "        elif self._default_section in sections:\n",
    "            config_sec = self._default_section\n",
    "        else:\n",
    "            raise ValueError(\"'%s' has more than one sections but there is no \"\n",
    "                             \"section named '%s'\" % filename, self._default_section)\n",
    "\n",
    "        config_arg = OrderedDict(config[config_sec].items())\n",
    "        for arg in self.cmd_arg:\n",
    "            if arg in config_arg:\n",
    "                config_arg[arg] = self.cmd_arg[arg]\n",
    "\n",
    "        return config_arg\n",
    "\n",
    "    def params_str(self):\n",
    "        \"\"\"Get a summary of parameters.\n",
    "        Returns:\n",
    "            str: A string summary of parameters.\n",
    "        \"\"\"\n",
    "        params_id = '_'.join([\"{}={}\".format(arg, value) for arg, value in self.alg_arg.items() if len(value) < 20])\n",
    "        special_char = {'/', '\\\\', '\\\"', ':', '*', '?', '<', '>', '|', '\\t'}\n",
    "        params_id = [c if c not in special_char else '_' for c in params_id]\n",
    "        params_id = ''.join(params_id)\n",
    "        params_id = \"%s_%s\" % (self[\"recommender\"], params_id)\n",
    "        return params_id\n",
    "\n",
    "    def __getitem__(self, item):\n",
    "        if not isinstance(item, str):\n",
    "            raise TypeError(\"index must be a str\")\n",
    "\n",
    "        if item in self.lib_arg:\n",
    "            param = self.lib_arg[item]\n",
    "        elif item in self.alg_arg:\n",
    "            param = self.alg_arg[item]\n",
    "        elif item in self.cmd_arg:\n",
    "            param = self.cmd_arg[item]\n",
    "        else:\n",
    "            raise KeyError(\"There are not the parameter named '%s'\" % item)\n",
    "\n",
    "        # convert param from str to value, i.e. int, float or list etc.\n",
    "        try:\n",
    "            value = eval(param)\n",
    "            if not isinstance(value, (str, int, float, list, tuple, bool, None.__class__)):\n",
    "                value = param\n",
    "        except:\n",
    "            if param.lower() == \"true\":\n",
    "                value = True\n",
    "            elif param.lower() == \"false\":\n",
    "                value = False\n",
    "            else:\n",
    "                value = param\n",
    "\n",
    "        return value\n",
    "\n",
    "    def __getattr__(self, item):\n",
    "        return self[item]\n",
    "\n",
    "    def __contains__(self, o):\n",
    "        return o in self.lib_arg or o in self.alg_arg or o in self.cmd_arg\n",
    "\n",
    "    def __str__(self):\n",
    "        lib_info = '\\n'.join([\"{}={}\".format(arg, value) for arg, value in self.lib_arg.items()])\n",
    "        alg_info = '\\n'.join([\"{}={}\".format(arg, value) for arg, value in self.alg_arg.items()])\n",
    "        info = \"\\n\\nNeuRec hyperparameters:\\n%s\\n\\n%s's hyperparameters:\\n%s\\n\" % (lib_info, self[\"recommender\"], alg_info)\n",
    "        return info\n",
    "\n",
    "    def __repr__(self):\n",
    "        return self.__str__()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Overwriting config.properties\n"
     ]
    }
   ],
   "source": [
    "#hide\n",
    "%%writefile config.properties\n",
    "[default]\n",
    "recommender=config_MF\n",
    "config_dir=.\n",
    "\n",
    "[mlp]\n",
    "recommender=config_MLP\n",
    "config_dir=."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "%%writefile config_MF.properties\n",
    "[hyperparameters]\n",
    "epochs=300\n",
    "batch_size='512'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "%%writefile config_MLP.properties\n",
    "[hyperparameters]\n",
    "epochs=100\n",
    "layers=[32,64,1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "testCustomSectionValue (__main__.TestConfigurator) ... ok\n",
      "testDefaultSectionValue (__main__.TestConfigurator) ... ok\n",
      "testIntegerType (__main__.TestConfigurator) ... ok\n",
      "testListType (__main__.TestConfigurator) ... ok\n",
      "testListValue (__main__.TestConfigurator) ... ok\n",
      "testRecommenderName (__main__.TestConfigurator) ... ok\n",
      "testRecommenderNameType (__main__.TestConfigurator) ... ok\n",
      "testUnwantedTypeConversion (__main__.TestConfigurator) ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 8 tests in 0.038s\n",
      "\n",
      "OK\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<unittest.main.TestProgram at 0x7f4771c904d0>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import unittest\n",
    "\n",
    "\n",
    "class TestConfigurator(unittest.TestCase):\n",
    "    def setUp(self):\n",
    "        self.conf_default = Configurator(\"config.properties\", default_section=\"default\")\n",
    "        self.conf_mlp = Configurator(\"config.properties\", default_section=\"mlp\")\n",
    "\n",
    "    def testRecommenderName(self):\n",
    "        self.assertEqual(self.conf_default[\"recommender\"], \"config_MF\")\n",
    "    \n",
    "    def testRecommenderNameType(self):\n",
    "        self.assertEqual(str(type(self.conf_default[\"recommender\"])), \"<class 'str'>\")\n",
    "\n",
    "    def testIntegerType(self):\n",
    "        self.assertEqual(str(type(self.conf_default[\"epochs\"])), \"<class 'int'>\")\n",
    "\n",
    "    def testListType(self):\n",
    "        self.assertEqual(str(type(self.conf_mlp[\"layers\"])), \"<class 'list'>\")\n",
    "    \n",
    "    def testUnwantedTypeConversion(self):\n",
    "        self.assertEqual(str(type(self.conf_default[\"batch_size\"])), \"<class 'str'>\")\n",
    "\n",
    "    def testDefaultSectionValue(self):\n",
    "        self.assertEqual(self.conf_default[\"epochs\"], 300)\n",
    "\n",
    "    def testCustomSectionValue(self):\n",
    "        self.assertEqual(self.conf_mlp[\"epochs\"], 100)\n",
    "    \n",
    "    def testListValue(self):\n",
    "        self.assertEqual(self.conf_mlp[\"layers\"], [32, 64, 1])\n",
    "\n",
    "\n",
    "unittest.main(argv=[''], verbosity=2, exit=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Author: Sparsh A.\n",
      "\n",
      "Last updated: 2021-12-05 09:19:00\n",
      "\n",
      "Compiler    : GCC 7.5.0\n",
      "OS          : Linux\n",
      "Release     : 5.4.104+\n",
      "Machine     : x86_64\n",
      "Processor   : x86_64\n",
      "CPU cores   : 2\n",
      "Architecture: 64bit\n",
      "\n",
      "sys    : 3.7.12 (default, Sep 10 2021, 00:21:48) \n",
      "[GCC 7.5.0]\n",
      "IPython: 5.5.0\n",
      "\n"
     ]
    }
   ],
   "source": [
    "#hide-input\n",
    "!pip install -q watermark\n",
    "%reload_ext watermark\n",
    "%watermark -a \"Sparsh A.\" -m -iv -u -t -d"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
