Skip to content

Commit bd8649b

Browse files
authored
Add basilisp.csv namespace (#1327)
1 parent 7c0aa93 commit bd8649b

4 files changed

Lines changed: 128 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
* Added `basilisp.csv` namespace (#753)
10+
811
### Changed
912
* Add `test` and `tests` directories to `PYTHONPATH` automatically during `basilisp test` CLI invocations (#1069)
1013

docs/api/csv.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
basilisp.csv
2+
============
3+
4+
.. toctree::
5+
:maxdepth: 2
6+
:caption: Contents:
7+
8+
.. autonamespace:: basilisp.csv
9+
:members:
10+
:undoc-members:

src/basilisp/csv.lpy

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
(ns basilisp.csv
2+
"Wrapper functions for :external:py:mod:`csv` which reads and writes CSVs."
3+
(:require
4+
[basilisp.io :as bio])
5+
(:import
6+
csv))
7+
8+
(defn read-csv
9+
"Returns a lazy sequence of vectors corresponding to the rows of a CSV file.
10+
11+
If ``input`` is a string, it will be treated as the contents of a CSV. Otherwise,
12+
it will be treated as a :external:py:class:`io.TextIOBase`.
13+
14+
The reader function supports the following keyword arguments:
15+
16+
:keyword ``:separator``: the character used as the separator for a fields in a row
17+
default is \",\"
18+
:keyword ``:quote``: the quote character used on quoted fields; default is ``true``"
19+
[input & {:keys [separator quote] :or {separator "," quote "\""}}]
20+
(let [f (if (string? input)
21+
(io/StringIO input)
22+
input)
23+
reader (csv/reader f ** :delimiter separator :quotechar quote)
24+
do-read (fn read-csv*
25+
[]
26+
(when-some [row (python/next reader nil)]
27+
(cons (vec row) (lazy-seq (read-csv*)))))]
28+
(lazy-seq (do-read))))
29+
30+
(defn rows->maps
31+
"Returns a lazy sequence mapping over a sequence of CSV data (as vectors)
32+
converting them to maps.
33+
34+
The first row will be used as the headers. The ``:key-fn`` keyword argument
35+
names a function which will be used to map over the keys."
36+
[rows & {:keys [key-fn] :or {key-fn keyword}}]
37+
(let [header (mapv key-fn (first rows))]
38+
(map #(zipmap header %) (rest rows))))
39+
40+
(defn write-csv
41+
"Write a sequence of rows in CSV format to ``writer``, which should be an
42+
:external:py:class:`io.TextIOBase`.
43+
44+
The writing function supports the following keyword arguments:
45+
46+
:keyword ``:separator``: the character used as the separator for a fields in a row
47+
default is \",\"
48+
:keyword ``:quote``: the quote character used on quoted fields; default is ``true``
49+
:keyword ``:newline``: the newline character to use between rows (``:lf`` or
50+
``:cr+lf``)"
51+
[writer data & {:keys [separator quote newline] :or {separator "," quote "\"" newline :lf}}]
52+
(let [nl (cond
53+
(= newline :lf) "\n"
54+
(= newline :cr+lf) "\r\n"
55+
:else (throw
56+
(ex-info "newline must be one of: :lf or :cr+lf"
57+
{:newline newline})))
58+
w (csv/writer writer ** :delimiter separator :quotechar quote :lineterminator nl)]
59+
(.writerows w (map #(mapv str %) data))))

tests/basilisp/test_csv.lpy

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
(ns tests.basilisp.test-csv
2+
(:require
3+
[basilisp.csv :as csv]
4+
[basilisp.test :refer [deftest is are testing]])
5+
(:import io))
6+
7+
(def test-csv-content
8+
"id,name,country
9+
1,John,United Kingdom
10+
2,Hanns,Germany
11+
3,Jose,Spain")
12+
13+
(def test-csv-headers
14+
["id" "name" "country"])
15+
16+
(def test-csv-rows
17+
'(["1" "John" "United Kingdom"]
18+
["2" "Hanns" "Germany"]
19+
["3" "Jose" "Spain"]))
20+
21+
(deftest read-csv-test
22+
(testing "string"
23+
(is (= '() (csv/read-csv "")))
24+
(is (= '() (csv/rows->maps (csv/read-csv "id,name"))))
25+
(is (= (concat [test-csv-headers] test-csv-rows)
26+
(csv/read-csv test-csv-content)))
27+
(is (= (map zipmap (repeat [:id :name :country]) test-csv-rows)
28+
(csv/rows->maps (csv/read-csv test-csv-content)))))
29+
30+
(testing "file-like object"
31+
(with-open [f (io/StringIO)]
32+
(is (= '() (csv/read-csv f))))
33+
(with-open [f (io/StringIO "id,name")]
34+
(is (= '() (csv/rows->maps (csv/read-csv f)))))
35+
(with-open [f (io/StringIO test-csv-content)]
36+
(is (= (concat [test-csv-headers] test-csv-rows)
37+
(csv/read-csv f))))
38+
(with-open [f (io/StringIO test-csv-content)]
39+
(is (= (map zipmap (repeat [:id :name :country]) test-csv-rows)
40+
(csv/rows->maps (csv/read-csv f)))))))
41+
42+
(deftest write-csv-test
43+
(is (thrown? basilisp.lang.exception/ExceptionInfo
44+
(csv/write-csv (io/StringIO) [] :newline :wrong-nl)))
45+
46+
(with-open [f (io/StringIO)]
47+
(csv/write-csv f [])
48+
(is (= "" (.getvalue f))))
49+
50+
(with-open [f (io/StringIO)]
51+
(csv/write-csv f [test-csv-headers])
52+
(is (= "id,name,country\n" (.getvalue f))))
53+
54+
(with-open [f (io/StringIO)]
55+
(csv/write-csv f (concat [test-csv-headers] test-csv-rows))
56+
(is (= (str test-csv-content "\n") (.getvalue f)))))

0 commit comments

Comments
 (0)