Skip to content

Commit e3981b4

Browse files
committed
annotation API: add printers and error handlers
1 parent 4b34329 commit e3981b4

File tree

13 files changed

+401
-10
lines changed

13 files changed

+401
-10
lines changed

argparse/src-3/argparse/core/Command.scala

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ object CommandMacros:
121121
val inner = rtpe.asType match
122122
case '[t] => findAllImpl[t]
123123

124+
val printerTpe = TypeSelect(api.asTerm, "Printer").tpe.appliedTo(List(rtpe))
125+
val printer = Implicits.search(printerTpe) match
126+
case iss: ImplicitSearchSuccess =>
127+
iss.tree
128+
case other =>
129+
report.error(s"No ${printerTpe.show} available for ${method.name}.", method.pos.get)
130+
'{???}.asTerm
131+
124132
val makeParser = '{
125133
(instance: () => Container) =>
126134
val parser = $api.ArgumentParser(description = ${Expr(doc.paragraphs.mkString("\n"))})
@@ -242,7 +250,7 @@ object CommandMacros:
242250
Expr.ofSeq(accessors)
243251
}
244252

245-
def callOrInstantiate() =
253+
def callOrInstantiate() = try
246254
val outer = instance()
247255
val results = args.map(_.map(_()))
248256
${
@@ -259,11 +267,24 @@ object CommandMacros:
259267
'results
260268
).asExpr
261269
}
270+
catch
271+
case t: Throwable =>
272+
$api.handleError(t)
262273

263274
${
264275
if inner.isEmpty then
265276
'{
266-
parser.action{callOrInstantiate()}
277+
parser.action{
278+
val p = $api
279+
val pr = ${printer.asExpr}.asInstanceOf[p.Printer[Any]]
280+
pr.print(
281+
callOrInstantiate(),
282+
System.out,
283+
OutputApi.StreamInfo(
284+
isatty = Platform.isConsole()
285+
)
286+
)
287+
}
267288
parser
268289
}
269290
else

argparse/src-3/argparse/core/MacroApi.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package argparse.core
22

3-
trait MacroApi extends TypesApi with ParsersApi:
3+
trait MacroApi extends TypesApi with ParsersApi with OutputApi:
44

55
// this will be used once macro-annotations are released
66
// class main() extends annotation.StaticAnnotation
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package argparse.core
2+
3+
import javax.swing.GroupLayout.Alignment
4+
5+
trait OutputApi extends ParsersApi with TypesApi with Printers:
6+
7+
/** Top-level error handler for command line applications using the annotation
8+
* API.
9+
*
10+
* You can override this to change what should be done on errors.
11+
*
12+
* The default implementation prints the exception's message unless the DEBUG
13+
* environment variable is set, in which case the whole stack trace is
14+
* printed. Then, it exits with error code 1.
15+
*/
16+
def handleError(t: Throwable): Nothing =
17+
if sys.env.contains("DEBUG") then
18+
t.printStackTrace()
19+
else
20+
System.err.println(t.getMessage())
21+
exit(1)
22+
23+
trait Printer[A]:
24+
def print(
25+
a: A,
26+
out: java.io.PrintStream,
27+
info: OutputApi.StreamInfo
28+
): Unit
29+
30+
object OutputApi:
31+
32+
case class StreamInfo(
33+
isatty: Boolean
34+
)
35+
36+
enum Alignment:
37+
case Left
38+
case Right
39+
case Numeric
40+
41+
def tabulate(
42+
rows: Iterable[Iterable[_]],
43+
header: Iterable[String] = Iterable.empty,
44+
alignments: Iterable[Alignment] = Iterable.empty
45+
): String = {
46+
if (header.isEmpty && rows.isEmpty) return ""
47+
48+
val ncols = if (!header.isEmpty) {
49+
header.size
50+
} else {
51+
rows.head.size
52+
}
53+
54+
val aligns = if (alignments.isEmpty) {
55+
if (!rows.isEmpty) {
56+
rows.head.map{ elem =>
57+
val s = elem.toString
58+
if (s.size > 0 && (s(0) == '-' || s(0).isDigit)) {
59+
Alignment.Numeric
60+
} else {
61+
Alignment.Left
62+
}
63+
}.toArray
64+
} else {
65+
header.map(_ => Alignment.Left).toArray
66+
}
67+
} else {
68+
alignments.toArray
69+
}
70+
var i = 0
71+
var j = 0
72+
73+
val leftMosts = Array.fill(ncols)(0)
74+
val rightMosts = Array.fill(ncols)(0)
75+
76+
val strings = for (row <- rows) yield {
77+
i = 0
78+
for (elem <- row) yield {
79+
val s = elem.toString
80+
81+
if (aligns(i) == Alignment.Numeric) {
82+
j = 0
83+
var commaPos = s.length
84+
while (j < s.length && commaPos == s.length) {
85+
if (s.charAt(j) == '.') commaPos = j
86+
j += 1
87+
}
88+
89+
if (commaPos > leftMosts(i)) leftMosts(i) = commaPos
90+
if ((s.length - commaPos) > rightMosts(i)) rightMosts(i) = s.length - commaPos
91+
} else {
92+
if (s.length > leftMosts(i)) leftMosts(i) = s.length
93+
}
94+
i += 1
95+
s
96+
}
97+
}
98+
99+
val maxWidths = Array.fill(ncols)(0)
100+
101+
val header1 = header.toArray
102+
103+
i = 0
104+
while (i < leftMosts.size) {
105+
val w = leftMosts(i) + rightMosts(i)
106+
if (w > maxWidths(i)) maxWidths(i) = w
107+
if (!header.isEmpty && header1(i).size > maxWidths(i)) maxWidths(i) = header1(i).size
108+
i += 1
109+
}
110+
111+
val buffer = new collection.mutable.StringBuilder
112+
val srows = strings.iterator
113+
114+
inline def printElemLeft(elem: String) = {
115+
buffer ++= elem
116+
val padding = maxWidths(i) - elem.length
117+
j = 0
118+
while (j < padding) {
119+
buffer += ' '
120+
j += 1
121+
}
122+
}
123+
124+
inline def printElemRight(elem: String) = {
125+
val padding = maxWidths(i) - elem.length
126+
j = 0
127+
while (j < padding) {
128+
buffer += ' '
129+
j += 1
130+
}
131+
buffer ++= elem
132+
}
133+
134+
inline def printElemNumeric(elem: String) = {
135+
j = 0
136+
var commaPos = elem.length
137+
while (j < elem.size && commaPos == elem.length) {
138+
if (elem.charAt(j) == '.') commaPos = j
139+
j += 1
140+
}
141+
142+
val paddingL = (maxWidths(i) - leftMosts(i) - rightMosts(i)) + leftMosts(i) - commaPos
143+
j = 0
144+
while (j < paddingL) {
145+
buffer += ' '
146+
j += 1
147+
}
148+
buffer ++= elem
149+
val paddingR = rightMosts(i) - (elem.length - commaPos)
150+
j = 0
151+
while (j < paddingR) {
152+
buffer += ' '
153+
j += 1
154+
}
155+
}
156+
157+
inline def printElem(elem: String) = aligns(i) match {
158+
case Alignment.Left => printElemLeft(elem)
159+
case Alignment.Right => printElemRight(elem)
160+
case Alignment.Numeric => printElemNumeric(elem)
161+
}
162+
163+
inline def printRow(elems: Iterator[String]) = {
164+
i = 0
165+
if (elems.hasNext) {
166+
printElem(elems.next)
167+
i += 1
168+
}
169+
while (elems.hasNext) {
170+
buffer += ' '
171+
printElem(elems.next)
172+
i += 1
173+
}
174+
}
175+
176+
if (!header.isEmpty) {
177+
val elems = header.iterator // if header is not empty, the first srows elem is the header
178+
i = 0
179+
if (elems.hasNext) {
180+
printElemLeft(elems.next)
181+
i += 1
182+
}
183+
while (elems.hasNext) {
184+
buffer += ' '
185+
printElemLeft(elems.next)
186+
i += 1
187+
}
188+
}
189+
190+
if (srows.hasNext) {
191+
if (!header.isEmpty) buffer += '\n'
192+
printRow(srows.next().iterator)
193+
}
194+
while (srows.hasNext) {
195+
buffer += '\n'
196+
printRow(srows.next().iterator)
197+
}
198+
buffer.toString
199+
}
200+
201+
def tabulatep(
202+
rows: Iterable[Product],
203+
headers: Iterable[String] = null,
204+
alignments: Iterable[Alignment] = Iterable.empty
205+
): String =
206+
tabulate(
207+
rows.map(_.productIterator.toIterable),
208+
if headers == null then
209+
if !rows.isEmpty then rows.head.productElementNames.toIterable.map(_.toUpperCase())
210+
else Iterable.empty
211+
else headers,
212+
alignments
213+
)
214+
215+
trait Printers extends LowPrioPrinters:
216+
self: OutputApi =>
217+
218+
given Printer[Unit] with
219+
def print(a: Unit, out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit = ()
220+
221+
given Printer[Array[Byte]] with
222+
def print(a: Array[Byte], out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit =
223+
out.write(a)
224+
225+
given Printer[geny.Writable] with
226+
def print(a: geny.Writable, out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit =
227+
a.writeBytesTo(out)
228+
229+
given [A](using p: Printer[A]): Printer[geny.Generator[A]] with
230+
def print(value: geny.Generator[A], out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit =
231+
value.foreach(v => p.print(v, out, info))
232+
233+
inline given productListPrinter[A <: Iterable[B], B <: Product](using m: ProductLabels[B]): Printer[A] with
234+
def print(value: A, out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit =
235+
val rows = value.map(_.productIterator.toIterable)
236+
if info.isatty then
237+
out.println(OutputApi.tabulate(rows, header = m.labels.map(_.toUpperCase)))
238+
else
239+
out.println(OutputApi.tabulate(rows, header = Iterable.empty))
240+
241+
given nonProductListPrinter[A <: Iterable[B], B](using elemPrinter: Printer[B]): Printer[A] with
242+
def print(value: A, out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit =
243+
for elem <- value do elemPrinter.print(elem, out, info)
244+
245+
given [A](using p: Printer[A]): Printer[concurrent.Future[A]] with
246+
def print(value: concurrent.Future[A], out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit =
247+
p.print(
248+
concurrent.Await.result(value, concurrent.duration.Duration.Inf),
249+
out,
250+
info
251+
)
252+
253+
trait LowPrioPrinters:
254+
self: OutputApi =>
255+
256+
// fallback to always print something
257+
given [A]: Printer[A] with
258+
def print(a: A, out: java.io.PrintStream, info: OutputApi.StreamInfo): Unit =
259+
out.println(a.toString)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package argparse.core
2+
3+
case class ProductLabels[A](
4+
labels: List[String]
5+
)
6+
7+
object ProductLabels:
8+
import quoted.Expr, quoted.Varargs, quoted.Type, quoted.Quotes
9+
10+
inline given [A <: Product]: ProductLabels[A] = of[A]
11+
12+
inline def of[A <: Product]: ProductLabels[A] = ${ofImpl[A]}
13+
14+
def ofImpl[A <: Product: Type](using qctx: Quotes): Expr[ProductLabels[A]] =
15+
import qctx.reflect.*
16+
17+
val tpe = TypeRepr.of[A]
18+
19+
if tpe.classSymbol.isDefined then
20+
val labels = for field <- tpe.classSymbol.get.caseFields yield
21+
Expr(field.name)
22+
'{
23+
ProductLabels[A](${Expr.ofList(labels)})
24+
}
25+
else
26+
'{ProductLabels[A](Nil)}
27+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package argparse.core
2+
3+
object Platform {
4+
def isConsole(): Boolean = System.console() != null
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package argparse.core
2+
3+
object Platform {
4+
import scalanative.unsafe._
5+
import scalanative.posix.sys.types
6+
7+
@extern
8+
object unistd {
9+
def isatty(fd: CInt): CInt = extern
10+
}
11+
12+
def isConsole(): Boolean = unistd.isatty(1) == 1
13+
}

argparse/src/argparse/core/TextUtils.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ object TextUtils {
4747
}
4848
kebab.result()
4949
}
50+
5051
}

0 commit comments

Comments
 (0)