Author: ehuelsmann Date: Sun Jan 6 15:59:07 2008 New Revision: 1
Added: branches/ developer-resources/ public_html/ tags/ trunk/ trunk/config.lisp trunk/package.lisp trunk/parser.lisp trunk/py-configparser.asd Log:
Added: trunk/config.lisp ============================================================================== --- (empty file) +++ trunk/config.lisp Sun Jan 6 15:59:07 2008 @@ -0,0 +1,293 @@ + +(cl:in-package :py-configparser) + +;; The conditions (errors) + +(define-condition configparser-error (error) ()) + +;; Errors for the configuration management side +(define-condition config-error (configparser-error) ()) +(define-condition no-section-error (config-error) ()) +(define-condition duplicate-section-error (config-error) ()) +(define-condition no-option-error (config-error) ()) +(define-condition interpolation-error (config-error) ()) +(define-condition interpolation-depth-error (interpolation-error) ()) +(define-condition interpolation-missing-option-error (interpolation-error) ()) +(define-condition interpolation-syntax-error (interpolation-error) ()) + + +;; +;; Configuration storage and management routines +;; + + +;; The structures +;; Note: because ABCL has issues with its CLOS support +;; (as per 1-1-2008), we use structures below to +;; be maximally portable. + + +(defstruct section + name + options) + +(defstruct config + (defaults (make-section :name "DEFAULT")) + sections + (option-name-transform-fn #'string-downcase) + (section-name-transform-fn #'identity)) + +(defun norm-option-name (config option-name) + (funcall (config-option-name-transform-fn config) option-name)) + +(defun norm-section-name (config section-name) + (funcall (config-section-name-transform-fn config) section-name)) + +(defun %validate-section-name (name) + (when (or (= 0 (length name)) + (find #] name) + (find #\Newline name) + (find #\Return name)) + (error 'no-section-error)) ;; Invalid section name, signal so. + name) + +(defun %validate-option-name (name) + (when (or (= 0 (length name)) + (eql (aref name 0) #[) + (find #\Space name) + (find #\Tab name) + (find #\Return name) + (find #\Newline name)) + (error 'no-option-error));; No such option error + name) + +;; non-API +(defun %get-section (config section-name) + (if (string= "DEFAULT" section-name) + (config-defaults config) + (let* ((norm-section-name (norm-section-name config section-name)) + (section (find norm-section-name (config-sections config) + :key #'section-name + :test #'string=))) + (unless section + (error 'no-section-error)) ;; no-such-section error + section))) + +;; non-API +(defun %get-option (config section-name option-name if-does-not-exist) + (let* ((section (%get-section config section-name)) + (norm-option (norm-option-name config option-name)) + (option (assoc norm-option + (section-options section) + :test #'string=))) + (if (null option) + (if (eq if-does-not-exist :error) + (error 'no-option-error) ;; no such option error + (values (car (push (list (%validate-option-name option-name)) + (section-options section))) + section)) + (values option section)))) + +;; +;; The API +;; + +(defun defaults (config) + "Returns an alist containing instance wide defaults, where the +elements are 2-element dotted lists: the CDR is the value +associated with the key." + (section-options (config-defaults config))) + +(defun sections (config) + "Returns a list of names of defined sections." + (mapcar #'section-name (config-sections config))) + +(defun has-section-p (config section-name) + "Returns `NIL' when the section is not added to the config yet, +some other value if it is." + (handler-case + (%get-section config section-name) + (no-section-error () nil))) + +(defun add-section (config section-name) + "Adds a new section to the config. + +If the section exists, the `duplicate-section-error' is raised." + (%validate-section-name section-name) + (let ((norm-section-name (funcall (config-section-name-transform-fn config) + section-name))) + (when (has-section-p config section-name) + (error 'duplicate-section-error)) + (car (push (make-section :name norm-section-name) + (config-sections config))))) + +(defun options (config section-name) + "Returns a list of option names which are defined in the given section." + (let ((section (%get-section config section-name))) + (mapcar #'first (section-options section)))) + +(defun has-option-p (config section-name option-name) + "Returns a generalised boolean with a value of `NIL' when +the specified option does not exist in the specified section +and some other value otherwise." + (handler-case + (%get-option config section-name option-name :error) + (no-option-error () nil))) + +;; non-API +(defun %extract-replacement (option-value) + ;; Returns: (VALUES replacement-option start end) or NIL + (let ((%-pos (position #% option-value))) + (when (and %-pos + (< (+ 3 %-pos) (length option-value)) + (eql (aref option-value (1+ %-pos)) #( )) + (let ((paren-pos (position #) option-value :start %-pos))) + (unless (and paren-pos + (< (1+ paren-pos) (length option-value)) + (eql (aref option-value (1+ paren-pos)) #\s)) + (error 'interpolation-syntax-error)) + ;; syntax error: %(..)s is minimally required + (when (<= 0 (- paren-pos %-pos 2)) + (let ((replacement-name + (make-array (- paren-pos %-pos 2) + :element-type (array-element-type option-value) + :displaced-to option-value + :displaced-index-offset (+ 2 %-pos)))) + (when (= 0 (length replacement-name)) + ;; some preconditions on replacement-name + (error 'interpolation-syntax-error)) + (values replacement-name %-pos (1+ paren-pos)))))))) + +;; non-API +(defun %option-value (config section option-name &key defaults) + (if (string= option-name "__name__") + (section-name section) + (let* ((norm-option-name (norm-option-name config option-name))) + (labels ((get-value (repositories) + (when (null repositories) + (error 'interpolation-missing-option-error)) + ;; no such option error + (let ((option (has-option-p config (section-name section) + option-name))) + (if option + (cdr option) + (get-value (cdr repositories)))))) + (get-value (list (section-options section) + defaults + (defaults config))))))) + +;; non-API +(defun %expand-option-value (config section option-value defaults + &optional dependees) + (multiple-value-bind + (replacement-name start end) + (%extract-replacement option-value) + (unless replacement-name + ;; nothing to do here... + (return-from %expand-option-value option-value)) + + (let ((norm-replacement (norm-option-name config replacement-name)) + (replacement-value (%option-value config section + replacement-name + :defaults defaults))) + (when (member norm-replacement dependees :test #'string=) + (error 'interpolation-depth-error)) ;; recursive dependency... + (%expand-option-value + config + section + (concatenate 'string + (subseq option-value 0 start) + (%expand-option-value config + section + replacement-value + defaults + (cons norm-replacement dependees)) + (subseq option-value (1+ end) (length option-value))) + defaults + dependees)))) + +(defun get-option (config section-name option-name + &key (expand t) defaults type) + "Returns the value of the specified option in the specified section. + +If `expand' is `NIL', any options which depend on other options +won't be expanded and the raw configuration value is returned. + +When `defaults' is an alist of which the elements are dotted lists of +key/value pairs, these values are used in the expansion of option values. + +`type' may be one of `:boolean', `:number' or it may remain unspecified." + (multiple-value-bind + (option section) + (%get-option config section-name option-name :error) + (flet ((convert-boolean (v) + (cond + ((member v '("1" "yes" "true" "on") :test #'string=) + T) + ((member v '("0" "no" "false" "off") :test #'string=) + NIL) + (t + (error 'not-a-boolean)))) + (convert-number (v) + (parse-number:parse-number v))) + (let ((string-value + (if expand + (%expand-option-value config + section (cdr option) + (list option-name)) + (cdr option)))) + (cond + ((eq type :boolean) + (convert-boolean string-value)) + ((eq type :number) + (convert-number string-value)) + ((null type) + string-value) + (t + (error "Illegal `type' parameter value."))))))) + +(defun set-option (config section-name option-name value) + "Sets the value of the specified option in the specified section. + +If the section does not exist, a `no-section-error' is raised. If the +option does not exist, it is created." + (let ((option (%get-option config section-name option-name :create))) + (setf (cdr option) value))) + +(defun items (config section-name &key (expand t) defaults) + "Returns an alist of which the items are dotted lists of key/value +pairs being the option names and values specified in the given section. + +When `expand' is `NIL', options are returned in raw form. Otherwise +option values are expanded. + +The definition of `defaults' is the same as for `get-option'." + (let ((section (get-section config section-name))) + (if expand + (mapcar #'(lambda (x) + (cons (car x) (get-option p section-name + (cdr x) ;; option-name + :expand t + :defaults defaults))) + (section-options section)) + (section-options section)))) + +(defun remove-option (config section-name option-name) + "Remove the specified option from the given section." + (multiple-value-bind + (option section) + (%get-option config section-name option-name :error) + (setf (section-options section) + (remove option (section-options section))))) + +(defun remove-section (config section-name) + "Remove the specified section. + +In case the section name equals the magic name `DEFAULT', +an error is raised, since this section can't be removed." + (when (string= section-name "DEFAULT") + (error 'no-section-error)) ;; no such section error + (let ((section (%get-section config section-name))) + (setf (config-sections config) + (remove section (config-sections config))))) +
Added: trunk/package.lisp ============================================================================== --- (empty file) +++ trunk/package.lisp Sun Jan 6 15:59:07 2008 @@ -0,0 +1,54 @@ + +;; This package is actuall two things: +;; 1) a configuration management utility +;; 2) a configuration file parser/writer in the .INI format +;; +;; But in the Python module this distinction hasn't been implemented +;; this stringently, meaning we're stuck to the current naming scheme. + +;; There's no reason however that you can't create your own format +;; and parse that, storing it in the config object as defined in this +;; package. (However, if you already use this module, you might as well +;; use the INI format as persistent format.) + + +(cl:defpackage #:py-configparser + (:use #:cl) + (:export + ;; common condition class + #:configparser-error + + ;; Configuration management + ;; Error classes + #:no-section-erorr + #:duplicate-section-error + #:no-option-error + #:interpolation-error + #:interpolation-depth-error + #:interpolation-missing-option-error + #:interpolation-syntax-error + + ;; Functions + #:make-config + #:defaults + #:sections + #:has-section-p + #:add-section + #:options + #:has-option-p + #:get-option + #:set-option + #:items + #:remove-option + #:remove-section + + ;; Configuration file parsing + ;; Error classes + #:parsing-error + #:missing-section-header-error + + ;; Functions + #:read-stream + #:read-files + #:write-stream)) +
Added: trunk/parser.lisp ============================================================================== --- (empty file) +++ trunk/parser.lisp Sun Jan 6 15:59:07 2008 @@ -0,0 +1,216 @@ + +(cl:in-package #:py-configparser) + +;; Errors for the parsing side + +(define-condition parsing-error (configparser-error) ()) +(define-condition missing-section-header-error (parsing-error) ()) + + + +;; The reader + +(proclaim '(special *line-no* *current-section* *file-name* + *current-input*)) +(proclaim '(inline %read-char %unread-char)) + +(defun %read-char (stream) + (let ((ch (read-char stream nil :eof))) + (when (eql ch #\Newline) + (incf *line-no*)) + (if (eq ch :eof) #\Newline ch))) + +(defun ensure-section (config section-name) + (handler-case + (%get-section config section-name) + (no-section-error () + (add-section config section-name))) + section-name) + +(defun is-whitespace (c) + (or (eq c #\Space) + (eq c #\Tab) + (eq c #\Return))) + +(defun is-comment-char (c) + (or (eq c #;) + (eq c ##))) + +(defun skip-whitespace (s) + (loop for c = (%read-char s) + while (is-whitespace c))) + +(defun skip-emtpy-line (s) + (loop for c = (%read-char s) + if (eq c #\Newline) do (return) + else unless (is-whitespace c) + do (error 'parsing-error))) ;; empty line expected + +(defun skip-to-eol (s) + (loop for c = (%read-char s) + until (eq c #\Newline))) + +(defun expect-char (s expect &key skip-whitespace) + (let ((ch (%read-char s))) + (when (and skip-whitespace + (is-whitespace ch)) + (loop for c = (%read-char s) + while (is-whitespace c) + finally (setf ch c))) + (unless (eq ch expect) + (error 'parsing-error)) ;; character expect expected, but ch found + ch)) + +(defun expect-one-of (s expect-bag &key skip-whitespace) + (let ((ch (%read-char s))) + (when (and skip-whitespace + (is-whitespace ch)) + (loop for c = (%read-char s) + while (is-whitespace c) + finally (setf ch c))) + (unless (member ch expect-bag) + (error 'parsing-error)) ;; character ch found, but looking for EXPECT-BAG + ch)) + +(defun make-input-buffer (p) + (make-array 20 :element-type 'cl:character :fill-pointer 0)) + +(proclaim '(inline extend-input)) +(defun extend-input (p c) + (vector-push-extend c *current-input* 20)) + +(defun finalize-input (p) + (let ((cp *current-input*)) + (setf *current-input* + (make-input-buffer p)) + cp)) + +(defun read-section-name (p s) + (expect-char s #[) + (loop for c = (%read-char s) + if (eq c #\Newline) + do (error 'parsing-error) ;; we can't have newlines in section names! + else if (eq c #]) + do (progn + (skip-to-eol s) + (return (finalize-input p))) + else do (extend-input p c))) + +(defun read-option-name (p s) + (loop for c = (%read-char s) + if (or (eq c #:) + (eq c #=)) + do (let ((option-name (finalize-input p))) + (when (= 0 (length option-name)) + (error 'parsing-error)) ;; No option name found + (return option-name)) + else if (is-whitespace c) + do (unread-char (expect-one-of s '(#: #=) :skip-whitespace t) s) + else do (extend-input p c))) + +(defun read-option-value (p s &key (leading-white :skip)) + (let ((leading-mode t) + (lead-detected nil)) + (loop for c = (%read-char s) + unless (or (eql c #\Return) + (eql c #\Newline)) + do (if (and leading-mode + (is-whitespace c)) + (setf lead-detected t) + (progn + (when (and (eq leading-white :fold) + leading-mode + lead-detected) + (extend-input p #\Space)) + (setf leading-mode nil) + (extend-input p c))) + + if (and (eql c #\Newline) + (let ((ch (peek-char nil s nil nil))) + (or (eql ch #\Space) + (eql ch #\Tab)))) + do (return (read-option-value p s :leading-white :fold)) + until (eql c #\Newline) + finally (return (finalize-input p))))) + +(defun reading-driver (p s) + (let ((*line-no* 0) + (*current-section* nil) + (*current-input* (make-input-buffer p))) + (loop for c = (peek-char nil s nil :eof) + until (eq c :eof) + if (eql c #[) + do (setf *current-section* + (ensure-section p (read-section-name p s))) + + else if (is-whitespace c) + do (skip-empty-line s) + + else if (is-comment-char c) + do (skip-to-eol s) + + else if (eql c #\Newline) + do (%read-char s) ;; skip over the newline character + + else do (if (null *current-section*) + (error 'missing-section-header-error) + (set-option p + *current-section* + (read-option-name p s) + (read-option-value p s)))))) + +;; +;; The API +;; + +(defun read-files (config filenames) + "Parses the files given in the list `filenames', if they exist. +The list is processed first to last, overwriting any pre-existing +values with the last value read. + +The results are stored in `config' which is modified destructively. + +Returns as values the configuration and the list of files actually read." + (let (files-read) + (dolist (filename (mapcar #'probe-file filenames) + (values config files-read)) + (with-open-file (s filename + :direction :input + :if-does-not-exist :error) + (read-stream config s :stream-name filename)) + (push filename files-read)))) + +(defun read-stream (config stream &key (stream-name "an unknown stream")) + "Parses the content of `stream' as a configuration file, +storing any values in `config' which is modified destructively. + +This function maps from the python 'readfp()' function." + (let ((*file-name* stream-name)) + (reading-driver config stream) + config ) + +(defun %format-value (value) + (if (and (numberp value) + (not (integerp value))) + (format nil "~,,,,,,'eE" value) + value)) + +(defun write-stream (config stream) + "Writes the configuration file corresponding to the +in-memory config state. Reloading the file +with `read-stream' or `read-files' will restore config state." + (flet ((write-section (section) + (format stream "[~a]~%" (section-name section)) + (format stream "~:{~A = ~{~A~%~}~}~%" + (mapcar #'(lambda (option) + (list (car option) + (list (%format-value (cdr option))))) + (section-options section))))) + (let ((*print-radix* nil) + (*print-base* 10)) + ;; set the printer output as expected by python + (when (defaults config) + ;; write the defaults too!! + (write-section (config-defaults config))) + (mapcar #'write-section (config-sections config))))) +
Added: trunk/py-configparser.asd ============================================================================== --- (empty file) +++ trunk/py-configparser.asd Sun Jan 6 15:59:07 2008 @@ -0,0 +1,20 @@ + + +(in-package #:cl-user) + +(defpackage #:py-configparser-system + (:use #:cl #:asdf)) + +(in-package #:py-configparser-system) + +(defsystem py-configparser + :name "py-configparser" + :author "Erik Huelsmann" + :version "1.0-dev" + :license "MIT" + :description "Common Lisp implementation of the Python ConfigParser module" + :depends-on (#:parse-number) + :components ((:file "package") + (:file "config" :depends-on ("package")) + (:file "parser" :depends-on ("config")))) +
py-configparser-cvs@common-lisp.net